@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.
- package/package.json +1 -1
- package/resources/ui-dynamic-form.umd.js +82 -33
- package/ui/components/UIDynamicForm.vue +622 -127
|
@@ -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
|
|
5
|
-
|
|
6
|
-
:
|
|
7
|
-
:
|
|
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
|
|
12
|
-
|
|
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"
|
|
24
|
+
:collapsed="collapsed"
|
|
25
|
+
:toggleCollapse="toggleCollapse"
|
|
26
|
+
/>
|
|
16
27
|
<Transition name="cardCollapse">
|
|
17
28
|
<div v-if="!collapsed">
|
|
18
|
-
<div
|
|
19
|
-
|
|
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
|
|
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
|
|
26
|
-
|
|
27
|
-
v-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
{{
|
|
80
|
+
{{
|
|
81
|
+
field.customForm ? JSON.parse(field.customForm).hint : undefined
|
|
50
82
|
}}
|
|
51
83
|
</p>
|
|
52
84
|
</div>
|
|
53
|
-
<component
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
296
|
-
|
|
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 =
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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>
|