@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0-feature-48cebe-mdiwg5tc → 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.
@@ -1,60 +1,98 @@
1
1
  <template>
2
2
  <div className="ui-dynamic-form-external-sizing-wrapper" :style="props.card_size_styling">
3
3
  <!-- Component must be wrapped in a block so props such as className and style can be passed in from parent -->
4
- <UIDynamicFormTitleText v-if="props.title_style === 'outside' && hasUserTask" :style="props.title_style"
5
- :title="effectiveTitle" :customStyles="props.title_custom_text_styling" :titleIcon="props.title_icon"
6
- :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)" :collapsed="collapsed"
7
- :toggleCollapse="toggleCollapse" />
4
+ <UIDynamicFormTitleText
5
+ v-if="props.title_style === 'outside' && hasUserTask"
6
+ :style="props.title_style"
7
+ :title="effectiveTitle"
8
+ :customStyles="props.title_custom_text_styling"
9
+ :titleIcon="props.title_icon"
10
+ :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
11
+ :collapsed="collapsed"
12
+ :toggleCollapse="toggleCollapse"
13
+ />
8
14
  <div className="ui-dynamic-form-wrapper">
9
15
  <p v-if="hasUserTask" style="margin-bottom: 0px">
10
16
  <v-form ref="form" v-model="form" :class="dynamicClass">
11
- <UIDynamicFormTitleText v-if="props.title_style != 'outside'" :style="props.title_style"
12
- :title="effectiveTitle" :customStyles="props.title_custom_text_styling"
17
+ <UIDynamicFormTitleText
18
+ v-if="props.title_style != 'outside'"
19
+ :style="props.title_style"
20
+ :title="effectiveTitle"
21
+ :customStyles="props.title_custom_text_styling"
13
22
  :titleIcon="props.title_icon"
14
23
  :collapsible="props.collapsible || (props.collapse_when_finished && formIsFinished)"
15
- :collapsed="collapsed" :toggleCollapse="toggleCollapse" />
24
+ :collapsed="collapsed"
25
+ :toggleCollapse="toggleCollapse"
26
+ />
16
27
  <Transition name="cardCollapse">
17
28
  <div v-if="!collapsed">
18
- <div className="ui-dynamic-form-formfield-positioner" :style="props.inner_card_styling"
19
- :data-columns="props.form_columns || 1">
29
+ <div
30
+ className="ui-dynamic-form-formfield-positioner"
31
+ :style="props.inner_card_styling"
32
+ :data-columns="props.form_columns || 1"
33
+ >
20
34
  <FormKit id="form" type="group">
21
- <v-row v-for="(field, index) in fields()" :key="field"
35
+ <v-row
36
+ v-for="(field, index) in fields()"
37
+ :key="field"
22
38
  :class="field.type === 'header' ? 'ui-dynamic-form-header-row' : ''"
23
- :style="getRowWidthStyling(field, index)">
39
+ :style="getRowWidthStyling(field, index)"
40
+ >
24
41
  <v-col cols="12">
25
- <component :is="createComponent(field).type"
26
- v-if="createComponent(field).innerHTML"
27
- v-bind="createComponent(field).props"
28
- :class="createComponent(field).class"
29
- v-html="createComponent(field).innerHTML" :ref="(el) => {
30
- if (index === 0) firstFormFieldRef = el;
31
- }
32
- " />
33
- <component :is="createComponent(field).type"
34
- v-else-if="createComponent(field).innerText"
35
- v-bind="createComponent(field).props" :ref="(el) => {
36
- if (index === 0) firstFormFieldRef = el;
37
- }
38
- " v-model="formData[field.id]">
39
- {{ createComponent(field).innerText }}
40
- </component>
41
- <div v-else-if="createComponent(field).type == 'v-slider'">
42
- <p class="formkit-label">{{ field.label }}</p>
43
- <component :is="createComponent(field).type"
44
- v-bind="createComponent(field).props" :ref="(el) => {
42
+ <component
43
+ :is="getFieldComponent(field).type"
44
+ v-if="getFieldComponent(field).innerHTML"
45
+ v-bind="getFieldComponent(field).props"
46
+ :class="getFieldComponent(field).class"
47
+ v-html="getFieldComponent(field).innerHTML"
48
+ :ref="
49
+ (el) => {
50
+ if (index === 0) firstFormFieldRef = el;
51
+ }
52
+ "
53
+ />
54
+ <component
55
+ :is="getFieldComponent(field).type"
56
+ v-else-if="getFieldComponent(field).innerText"
57
+ v-bind="getFieldComponent(field).props"
58
+ :ref="
59
+ (el) => {
45
60
  if (index === 0) firstFormFieldRef = el;
46
61
  }
47
- " v-model="field.defaultValue" />
62
+ "
63
+ v-model="formData[field.id]"
64
+ >
65
+ {{ getFieldComponent(field).innerText }}
66
+ </component>
67
+ <div v-else-if="getFieldComponent(field).type == 'v-slider'">
68
+ <p class="formkit-label">{{ field.label }}</p>
69
+ <component
70
+ :is="getFieldComponent(field).type"
71
+ v-bind="getFieldComponent(field).props"
72
+ :ref="
73
+ (el) => {
74
+ if (index === 0) firstFormFieldRef = el;
75
+ }
76
+ "
77
+ v-model="field.defaultValue"
78
+ />
48
79
  <p class="formkit-help">
49
- {{ field.customForm ? JSON.parse(field.customForm).hint : undefined
80
+ {{
81
+ field.customForm ? JSON.parse(field.customForm).hint : undefined
50
82
  }}
51
83
  </p>
52
84
  </div>
53
- <component :is="createComponent(field).type" v-else
54
- v-bind="createComponent(field).props" :ref="(el) => {
55
- if (index === 0) firstFormFieldRef = el;
56
- }
57
- " v-model="formData[field.id]" />
85
+ <component
86
+ :is="getFieldComponent(field).type"
87
+ v-else
88
+ v-bind="getFieldComponent(field).props"
89
+ :ref="
90
+ (el) => {
91
+ if (index === 0) firstFormFieldRef = el;
92
+ }
93
+ "
94
+ v-model="formData[field.id]"
95
+ />
58
96
  </v-col>
59
97
  </v-row>
60
98
  </FormKit>
@@ -63,17 +101,24 @@
63
101
  <v-row v-if="errorMsg.length > 0" style="padding: 12px">
64
102
  <v-alert type="error">Error: {{ errorMsg }}</v-alert>
65
103
  </v-row>
66
- <UIDynamicFormFooterAction v-if="props.actions_inside_card && actions.length > 0"
67
- :actions="actions" :actionCallback="actionFn" :formIsFinished="formIsFinished"
68
- style="padding: 16px; padding-top: 0px" />
104
+ <UIDynamicFormFooterAction
105
+ v-if="props.actions_inside_card && actions.length > 0"
106
+ :actions="actions"
107
+ :actionCallback="actionFn"
108
+ :formIsFinished="formIsFinished"
109
+ style="padding: 16px; padding-top: 0px"
110
+ />
69
111
  </v-row>
70
112
  </div>
71
113
  </Transition>
72
114
  </v-form>
73
115
  </p>
74
116
  <p v-else>
75
- <v-alert v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
76
- :text="props.waiting_info" :title="props.waiting_title" />
117
+ <v-alert
118
+ v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
119
+ :text="props.waiting_info"
120
+ :title="props.waiting_title"
121
+ />
77
122
  </p>
78
123
  </div>
79
124
  <div v-if="!props.actions_inside_card && actions.length > 0 && hasUserTask" style="padding-top: 32px">
@@ -94,8 +139,6 @@ import UIDynamicFormFooterAction from './FooterActions.vue';
94
139
  import UIDynamicFormTitleText from './TitleText.vue';
95
140
 
96
141
  function requiredIf({ value }, [targetField, expectedValue], node) {
97
- console.debug(arguments);
98
-
99
142
  const actual = node?.root?.value?.[targetField];
100
143
  const isEmpty = value === '' || value === null || value === undefined;
101
144
 
@@ -156,9 +199,6 @@ export default {
156
199
  },
157
200
  },
158
201
  setup(props) {
159
- console.info('UIDynamicForm setup with:', props);
160
- console.debug('Vue function loaded correctly', markRaw);
161
-
162
202
  const instance = getCurrentInstance();
163
203
  const app = instance.appContext.app;
164
204
 
@@ -166,7 +206,6 @@ export default {
166
206
  theme: 'genesis',
167
207
  locales: { de },
168
208
  locale: 'de',
169
- // eslint-disable-next-line object-shorthand
170
209
  rules: { requiredIf: requiredIf },
171
210
  });
172
211
  app.use(plugin, formkitConfig);
@@ -182,6 +221,8 @@ export default {
182
221
  msg: null,
183
222
  collapsed: false,
184
223
  firstFormFieldRef: null,
224
+ intersectionObserver: null,
225
+ visibleFileFields: new Set(),
185
226
  };
186
227
  },
187
228
  computed: {
@@ -202,7 +243,7 @@ export default {
202
243
  );
203
244
  },
204
245
  isConfirmDialog() {
205
- return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm');
246
+ return this.userTask?.userTaskConfig?.formFields?.some((field) => field.type === 'confirm') || false;
206
247
  },
207
248
  effectiveTitle() {
208
249
  if (this.props.title_text_type === 'str') {
@@ -213,6 +254,29 @@ export default {
213
254
  return '';
214
255
  }
215
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
+ },
216
280
  },
217
281
  watch: {
218
282
  formData: {
@@ -266,6 +330,9 @@ export default {
266
330
  element.classList.add('test');
267
331
  });
268
332
 
333
+ // Initialize Intersection Observer for lazy loading
334
+ this.initLazyLoading();
335
+
269
336
  this.$socket.on('widget-load:' + this.id, (msg) => {
270
337
  this.init(msg);
271
338
  });
@@ -280,86 +347,161 @@ export default {
280
347
  /* Make sure, any events you subscribe to on SocketIO are unsubscribed to here */
281
348
  this.$socket?.off('widget-load' + this.id);
282
349
  this.$socket?.off('msg-input:' + this.id);
350
+
351
+ // Clean up Intersection Observer
352
+ if (this.intersectionObserver) {
353
+ this.intersectionObserver.disconnect();
354
+ }
283
355
  },
284
356
  methods: {
357
+ // Simplified component getter - now just returns from computed cache
358
+ getFieldComponent(field) {
359
+ return this.fieldComponents[field.id] || this.createComponent(field);
360
+ },
361
+
362
+ // Clear cache when form data changes
363
+ clearComponentCache() {
364
+ // This is now handled by computed properties automatically
365
+ },
366
+
285
367
  createComponent(field) {
286
- console.debug('Creating component for field:', field);
287
368
  const customForm = field.customForm ? JSON.parse(field.customForm) : {};
288
- const hint = customForm.hint;
289
- const placeholder = customForm.placeholder;
290
- const validation = customForm.validation;
369
+ const { hint, placeholder, validation, customProperties = [] } = customForm;
291
370
  const name = field.id;
292
- const customProperties = customForm.customProperties ?? [];
293
371
  const isReadOnly =
294
372
  this.props.readonly ||
295
- this.formIsFinished ||
296
- customProperties.find((entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true')
373
+ this.formIsFinished ||
374
+ customProperties.some(
375
+ (entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true'
376
+ )
297
377
  ? 'true'
298
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
+
299
396
  switch (field.type) {
300
397
  case 'long':
301
398
  return {
302
399
  type: 'FormKit',
303
400
  props: {
401
+ ...commonFormKitProps,
304
402
  type: 'number',
305
- id: field.id,
306
- name,
307
- label: field.label,
308
- required: field.required,
309
- value: this.formData[field.id],
310
403
  number: 'integer',
311
404
  min: 0,
312
405
  validation: validation ? `${validation}|number` : 'number',
313
- help: hint,
314
- wrapperClass: '$remove:formkit-wrapper',
315
- labelClass: 'ui-dynamic-form-input-label',
316
- inputClass: `input-${this.theme}`,
317
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
318
- readonly: isReadOnly,
319
- validationVisibility: 'live',
320
406
  },
321
407
  };
322
408
  case 'number':
323
- const step = field.customForm ? JSON.parse(field.customForm).step : undefined;
409
+ const step = customForm.step;
324
410
  return {
325
411
  type: 'FormKit',
326
412
  props: {
413
+ ...commonFormKitProps,
327
414
  type: 'number',
328
- id: field.id,
329
- name,
330
- label: field.label,
331
- required: field.required,
332
- value: this.formData[field.id],
333
415
  step,
334
416
  number: 'float',
335
417
  validation: validation ? `${validation}|number` : 'number',
336
- help: hint,
337
- wrapperClass: '$remove:formkit-wrapper',
338
- labelClass: 'ui-dynamic-form-input-label',
339
- inputClass: `input-${this.theme}`,
340
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
341
- readonly: isReadOnly,
342
- validationVisibility: 'live',
343
418
  },
344
419
  };
345
420
  case 'date':
346
421
  return {
347
422
  type: 'FormKit',
348
423
  props: {
424
+ ...commonFormKitProps,
349
425
  type: 'date',
350
- id: field.id,
351
- name,
352
- label: field.label,
353
- required: field.required,
354
- value: this.formData[field.id],
355
- help: hint,
356
- wrapperClass: '$remove:formkit-wrapper',
357
- labelClass: 'ui-dynamic-form-input-label',
358
- inputClass: `input-${this.theme}`,
359
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
360
- readonly: isReadOnly,
361
- validation,
362
- 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',
363
505
  },
364
506
  };
365
507
  case 'enum':
@@ -380,7 +522,9 @@ export default {
380
522
  wrapperClass: '$remove:formkit-wrapper',
381
523
  labelClass: 'ui-dynamic-form-input-label',
382
524
  inputClass: `input-${this.theme}`,
383
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
525
+ innerClass: `ui-dynamic-form-input-outlines ${
526
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
527
+ }`,
384
528
  readonly: isReadOnly,
385
529
  disabled: isReadOnly,
386
530
  validation,
@@ -406,7 +550,9 @@ export default {
406
550
  wrapperClass: '$remove:formkit-wrapper',
407
551
  labelClass: 'ui-dynamic-form-input-label',
408
552
  inputClass: `input-${this.theme}`,
409
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
553
+ innerClass: `ui-dynamic-form-input-outlines ${
554
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
555
+ }`,
410
556
  readonly: isReadOnly,
411
557
  disabled: isReadOnly,
412
558
  validation,
@@ -428,7 +574,9 @@ export default {
428
574
  wrapperClass: '$remove:formkit-wrapper',
429
575
  labelClass: 'ui-dynamic-form-input-label',
430
576
  inputClass: `input-${this.theme}`,
431
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
577
+ innerClass: `ui-dynamic-form-input-outlines ${
578
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
579
+ }`,
432
580
  readonly: isReadOnly,
433
581
  validation,
434
582
  validationVisibility: 'live',
@@ -452,13 +600,84 @@ export default {
452
600
  help: hint,
453
601
  labelClass: 'ui-dynamic-form-input-label',
454
602
  inputClass: `input-${this.theme}`,
455
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
603
+ innerClass: `ui-dynamic-form-input-outlines ${
604
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
605
+ }`,
456
606
  readonly: isReadOnly,
457
607
  disabled: isReadOnly,
458
608
  validation,
459
609
  validationVisibility: 'live',
460
610
  },
461
611
  };
612
+ case 'file-preview':
613
+ // Handle file preview display only (no upload functionality)
614
+ const originalFieldId = field.id.replace('_preview', '');
615
+ if (this.formData && this.formData[originalFieldId] && this.formData[originalFieldId].length != 0) {
616
+ const fileDataArray = Array.isArray(this.formData[originalFieldId])
617
+ ? this.formData[originalFieldId]
618
+ : [this.formData[originalFieldId]];
619
+
620
+ // Create unique container ID for this field
621
+ const containerId = `file-preview-${field.id}`;
622
+
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>
634
+ </div>
635
+ </div>
636
+ `;
637
+
638
+ // Process files asynchronously
639
+ setTimeout(() => {
640
+ this.processFilePreview(containerId, fileDataArray, field);
641
+ }, 0);
642
+
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>
662
+ </div>
663
+ </div>
664
+ `;
665
+
666
+ return {
667
+ type: 'div',
668
+ props: {
669
+ innerHTML: lazyContent,
670
+ },
671
+ };
672
+ }
673
+ }
674
+ // If no files to preview, return empty div
675
+ return {
676
+ type: 'div',
677
+ props: {
678
+ style: 'display: none;',
679
+ },
680
+ };
462
681
  case 'file':
463
682
  const multiple = field.customForm ? JSON.parse(field.customForm).multiple === 'true' : false;
464
683
  return {
@@ -469,13 +688,11 @@ export default {
469
688
  name,
470
689
  label: field.label,
471
690
  required: field.required,
472
- value: this.formData[field.id],
473
691
  help: hint,
474
692
  innerClass: 'reset-background',
475
693
  wrapperClass: '$remove:formkit-wrapper',
476
694
  labelClass: 'ui-dynamic-form-input-label',
477
695
  inputClass: `input-${this.theme}`,
478
- // innerClass: ui-dynamic-form-input-outlines `${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
479
696
  readonly: isReadOnly,
480
697
  disabled: isReadOnly,
481
698
  multiple,
@@ -501,7 +718,9 @@ export default {
501
718
  fieldsetClass: 'custom-fieldset',
502
719
  labelClass: 'ui-dynamic-form-input-label',
503
720
  inputClass: `input-${this.theme}`,
504
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
721
+ innerClass: `ui-dynamic-form-input-outlines ${
722
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
723
+ }`,
505
724
  readonly: isReadOnly,
506
725
  disabled: isReadOnly,
507
726
  validation,
@@ -539,7 +758,9 @@ export default {
539
758
  wrapperClass: '$remove:formkit-wrapper',
540
759
  labelClass: 'ui-dynamic-form-input-label',
541
760
  inputClass: `input-${this.theme}`,
542
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
761
+ innerClass: `ui-dynamic-form-input-outlines ${
762
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
763
+ }`,
543
764
  readonly: isReadOnly,
544
765
  validation,
545
766
  validationVisibility: 'live',
@@ -560,7 +781,9 @@ export default {
560
781
  wrapperClass: '$remove:formkit-wrapper',
561
782
  labelClass: 'ui-dynamic-form-input-label',
562
783
  inputClass: `input-${this.theme}`,
563
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
784
+ innerClass: `ui-dynamic-form-input-outlines ${
785
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
786
+ }`,
564
787
  readonly: isReadOnly,
565
788
  validation,
566
789
  validationVisibility: 'live',
@@ -600,7 +823,9 @@ export default {
600
823
  wrapperClass: '$remove:formkit-wrapper',
601
824
  labelClass: 'ui-dynamic-form-input-label',
602
825
  inputClass: `input-${this.theme}`,
603
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
826
+ innerClass: `ui-dynamic-form-input-outlines ${
827
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
828
+ }`,
604
829
  readonly: isReadOnly,
605
830
  validation,
606
831
  validationVisibility: 'live',
@@ -629,7 +854,9 @@ export default {
629
854
  wrapperClass: '$remove:formkit-wrapper',
630
855
  labelClass: 'ui-dynamic-form-input-label',
631
856
  inputClass: `input-${this.theme}`,
632
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
857
+ innerClass: `ui-dynamic-form-input-outlines ${
858
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
859
+ }`,
633
860
  readonly: isReadOnly,
634
861
  validation,
635
862
  validationVisibility: 'live',
@@ -653,7 +880,9 @@ export default {
653
880
  fieldsetClass: 'custom-fieldset',
654
881
  labelClass: 'ui-dynamic-form-input-label',
655
882
  inputClass: `input-${this.theme}`,
656
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
883
+ innerClass: `ui-dynamic-form-input-outlines ${
884
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
885
+ }`,
657
886
  readonly: isReadOnly,
658
887
  disabled: isReadOnly,
659
888
  validation,
@@ -700,7 +929,9 @@ export default {
700
929
  wrapperClass: '$remove:formkit-wrapper',
701
930
  labelClass: 'ui-dynamic-form-input-label',
702
931
  inputClass: `input-${this.theme}`,
703
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
932
+ innerClass: `ui-dynamic-form-input-outlines ${
933
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
934
+ }`,
704
935
  readonly: isReadOnly,
705
936
  validation,
706
937
  validationVisibility: 'live',
@@ -723,7 +954,9 @@ export default {
723
954
  wrapperClass: '$remove:formkit-wrapper',
724
955
  labelClass: 'ui-dynamic-form-input-label',
725
956
  inputClass: `input-${this.theme}`,
726
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
957
+ innerClass: `ui-dynamic-form-input-outlines ${
958
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
959
+ }`,
727
960
  readonly: isReadOnly,
728
961
  validation,
729
962
  validationVisibility: 'live',
@@ -744,7 +977,9 @@ export default {
744
977
  wrapperClass: '$remove:formkit-wrapper',
745
978
  labelClass: 'ui-dynamic-form-input-label',
746
979
  inputClass: `input-${this.theme}`,
747
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
980
+ innerClass: `ui-dynamic-form-input-outlines ${
981
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
982
+ }`,
748
983
  readonly: isReadOnly,
749
984
  validation,
750
985
  validationVisibility: 'live',
@@ -765,7 +1000,9 @@ export default {
765
1000
  wrapperClass: '$remove:formkit-wrapper',
766
1001
  labelClass: 'ui-dynamic-form-input-label',
767
1002
  inputClass: `input-${this.theme}`,
768
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1003
+ innerClass: `ui-dynamic-form-input-outlines ${
1004
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
1005
+ }`,
769
1006
  readonly: isReadOnly,
770
1007
  validation,
771
1008
  validationVisibility: 'live',
@@ -786,7 +1023,9 @@ export default {
786
1023
  wrapperClass: '$remove:formkit-wrapper',
787
1024
  labelClass: 'ui-dynamic-form-input-label',
788
1025
  inputClass: `input-${this.theme}`,
789
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1026
+ innerClass: `ui-dynamic-form-input-outlines ${
1027
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
1028
+ }`,
790
1029
  readonly: isReadOnly,
791
1030
  validation,
792
1031
  validationVisibility: 'live',
@@ -805,7 +1044,9 @@ export default {
805
1044
  help: hint,
806
1045
  labelClass: 'ui-dynamic-form-input-label',
807
1046
  inputClass: `input-${this.theme}`,
808
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1047
+ innerClass: `ui-dynamic-form-input-outlines ${
1048
+ this.theme === 'dark' ? '$remove:formkit-inner' : ''
1049
+ }`,
809
1050
  readonly: isReadOnly,
810
1051
  validation,
811
1052
  validationVisibility: 'live',
@@ -813,6 +1054,220 @@ export default {
813
1054
  };
814
1055
  }
815
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
+ },
816
1271
  toggleCollapse() {
817
1272
  this.collapsed = !this.collapsed;
818
1273
  },
@@ -829,13 +1284,7 @@ export default {
829
1284
  return style;
830
1285
  },
831
1286
  fields() {
832
- const aFields = this.userTask.userTaskConfig?.formFields ?? [];
833
- const fieldMap = aFields.map((field) => ({
834
- ...field,
835
- items: mapItems(field.type, field),
836
- }));
837
-
838
- return fieldMap;
1287
+ return this.computedFields;
839
1288
  },
840
1289
  /*
841
1290
  widget-action just sends a msg to Node-RED, it does not store the msg state server-side
@@ -848,6 +1297,8 @@ export default {
848
1297
  },
849
1298
  init(msg) {
850
1299
  this.msg = msg;
1300
+ this.clearComponentCache();
1301
+
851
1302
  if (!msg) {
852
1303
  return;
853
1304
  }
@@ -861,18 +1312,23 @@ export default {
861
1312
  } else {
862
1313
  this.userTask = null;
863
1314
  this.formData = {};
1315
+ // Reset lazy loading state
1316
+ this.visibleFileFields.clear();
864
1317
  return;
865
1318
  }
866
1319
 
867
1320
  const formFields = this.userTask.userTaskConfig.formFields;
868
1321
  const formFieldIds = formFields.map((ff) => ff.id);
869
- const initialValues = this.userTask.startToken;
1322
+ const initialValues = this.userTask.startToken.formData;
870
1323
  const finishedFormData = msg.payload.formData;
871
1324
  this.formIsFinished = !!msg.payload.formData;
872
1325
  if (this.formIsFinished) {
873
1326
  this.collapsed = this.props.collapse_when_finished;
874
1327
  }
875
1328
 
1329
+ // Reset lazy loading state for new task
1330
+ this.visibleFileFields.clear();
1331
+
876
1332
  if (formFields) {
877
1333
  formFields.forEach((field) => {
878
1334
  this.formData[field.id] = field.defaultValue;
@@ -897,6 +1353,18 @@ export default {
897
1353
  ];
898
1354
  }
899
1355
  });
1356
+
1357
+ // Check for file fields and duplicate them as file-preview if initial values exist
1358
+ // Insert preview fields directly before their corresponding file fields
1359
+ for (let i = formFields.length - 1; i >= 0; i--) {
1360
+ const field = formFields[i];
1361
+ if (field.type === 'file' && initialValues && initialValues[field.id]) {
1362
+ const previewField = { ...field };
1363
+ previewField.type = 'file-preview';
1364
+ previewField.id = `${field.id}_preview`; // Give it a unique ID
1365
+ this.userTask.userTaskConfig.formFields.splice(i, 0, previewField);
1366
+ }
1367
+ }
900
1368
  }
901
1369
 
902
1370
  if (initialValues) {
@@ -915,8 +1383,11 @@ export default {
915
1383
  });
916
1384
  }
917
1385
 
918
- nextTick(() => {
1386
+ // Force update of computed properties by triggering reactivity
1387
+ this.$nextTick(() => {
919
1388
  this.focusFirstFormField();
1389
+ // Re-observe lazy elements after DOM update
1390
+ this.observeLazyElements();
920
1391
  });
921
1392
  },
922
1393
  actionFn(action) {
@@ -967,7 +1438,7 @@ export default {
967
1438
  this.send(
968
1439
  msg,
969
1440
  this.actions.findIndex((element) => element.label === action.label) +
970
- (this.isConfirmDialog ? this.props.options.length : 0)
1441
+ (this.isConfirmDialog ? this.props.options.length : 0)
971
1442
  );
972
1443
  // TODO: mm - end
973
1444
  } else {
@@ -982,7 +1453,6 @@ export default {
982
1453
  const result = func(this.formData, this.userTask, this.msg);
983
1454
  return Boolean(result);
984
1455
  } catch (err) {
985
- console.error('Error while evaluating condition: ' + err);
986
1456
  return false;
987
1457
  }
988
1458
  },
@@ -1003,14 +1473,14 @@ export default {
1003
1473
  if (['INPUT', 'TEXTAREA', 'SELECT'].includes(this.firstFormFieldRef.$el.tagName)) {
1004
1474
  inputElement = this.firstFormFieldRef.$el;
1005
1475
  } else {
1006
- inputElement = this.firstFormFieldRef.$el.querySelector('input:not([type="hidden"]), textarea, select');
1476
+ inputElement = this.firstFormFieldRef.$el.querySelector(
1477
+ 'input:not([type="hidden"]), textarea, select'
1478
+ );
1007
1479
  }
1008
1480
  }
1009
1481
 
1010
1482
  if (inputElement) {
1011
1483
  inputElement.focus();
1012
- } else {
1013
- console.warn('Could not find a focusable input element for the first form field.');
1014
1484
  }
1015
1485
  }
1016
1486
  },
@@ -1032,4 +1502,29 @@ function mapItems(type, field) {
1032
1502
  <style>
1033
1503
  /* CSS is auto scoped, but using named classes is still recommended */
1034
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
+ }
1035
1530
  </style>