@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0-file-preview-ea90da-mdpynnv5 → 2.1.0

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