@5minds/node-red-dashboard-2-processcube-dynamic-form 2.1.0-file-preview-1bee64-mdpzqcz2 → 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,96 +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
- {{ getFieldHint(field) }}
49
+ {{ field.customForm ? JSON.parse(field.customForm).hint : undefined
50
+ }}
81
51
  </p>
82
52
  </div>
83
- <component
84
- :is="getFieldComponent(field).type"
85
- v-else
86
- v-bind="getFieldComponent(field).props"
87
- :ref="
88
- (el) => {
89
- if (index === 0) firstFormFieldRef = el;
90
- }
91
- "
92
- v-model="formData[field.id]"
93
- />
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]" />
94
58
  </v-col>
95
59
  </v-row>
96
60
  </FormKit>
@@ -99,24 +63,17 @@
99
63
  <v-row v-if="errorMsg.length > 0" style="padding: 12px">
100
64
  <v-alert type="error">Error: {{ errorMsg }}</v-alert>
101
65
  </v-row>
102
- <UIDynamicFormFooterAction
103
- v-if="props.actions_inside_card && actions.length > 0"
104
- :actions="actions"
105
- :actionCallback="actionFn"
106
- :formIsFinished="formIsFinished"
107
- style="padding: 16px; padding-top: 0px"
108
- />
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" />
109
69
  </v-row>
110
70
  </div>
111
71
  </Transition>
112
72
  </v-form>
113
73
  </p>
114
74
  <p v-else>
115
- <v-alert
116
- v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
117
- :text="props.waiting_info"
118
- :title="props.waiting_title"
119
- />
75
+ <v-alert v-if="props.waiting_info.length > 0 || props.waiting_title.length > 0"
76
+ :text="props.waiting_info" :title="props.waiting_title" />
120
77
  </p>
121
78
  </div>
122
79
  <div v-if="!props.actions_inside_card && actions.length > 0 && hasUserTask" style="padding-top: 32px">
@@ -137,6 +94,8 @@ import UIDynamicFormFooterAction from './FooterActions.vue';
137
94
  import UIDynamicFormTitleText from './TitleText.vue';
138
95
 
139
96
  function requiredIf({ value }, [targetField, expectedValue], node) {
97
+ console.debug(arguments);
98
+
140
99
  const actual = node?.root?.value?.[targetField];
141
100
  const isEmpty = value === '' || value === null || value === undefined;
142
101
 
@@ -197,6 +156,9 @@ export default {
197
156
  },
198
157
  },
199
158
  setup(props) {
159
+ console.info('UIDynamicForm setup with:', props);
160
+ console.debug('Vue function loaded correctly', markRaw);
161
+
200
162
  const instance = getCurrentInstance();
201
163
  const app = instance.appContext.app;
202
164
 
@@ -204,6 +166,7 @@ export default {
204
166
  theme: 'genesis',
205
167
  locales: { de },
206
168
  locale: 'de',
169
+ // eslint-disable-next-line object-shorthand
207
170
  rules: { requiredIf: requiredIf },
208
171
  });
209
172
  app.use(plugin, formkitConfig);
@@ -219,8 +182,6 @@ export default {
219
182
  msg: null,
220
183
  collapsed: false,
221
184
  firstFormFieldRef: null,
222
- intersectionObserver: null,
223
- visibleFileFields: new Set(),
224
185
  };
225
186
  },
226
187
  computed: {
@@ -241,7 +202,7 @@ export default {
241
202
  );
242
203
  },
243
204
  isConfirmDialog() {
244
- return this.userTask?.userTaskConfig?.formFields?.some((field) => field.type === 'confirm') || false;
205
+ return this.userTask.userTaskConfig.formFields.some((field) => field.type === 'confirm');
245
206
  },
246
207
  effectiveTitle() {
247
208
  if (this.props.title_text_type === 'str') {
@@ -252,29 +213,6 @@ export default {
252
213
  return '';
253
214
  }
254
215
  },
255
- // Optimized computed property for field components
256
- fieldComponents() {
257
- if (!this.userTask?.userTaskConfig?.formFields) {
258
- return {};
259
- }
260
-
261
- const components = {};
262
- const aFields = this.userTask.userTaskConfig.formFields;
263
-
264
- aFields.forEach((field) => {
265
- components[field.id] = this.createComponent(field);
266
- });
267
-
268
- return components;
269
- },
270
- // Optimized computed property for fields
271
- computedFields() {
272
- const aFields = this.userTask?.userTaskConfig?.formFields ?? [];
273
- return aFields.map((field) => ({
274
- ...field,
275
- items: mapItems(field.type, field),
276
- }));
277
- },
278
216
  },
279
217
  watch: {
280
218
  formData: {
@@ -328,9 +266,6 @@ export default {
328
266
  element.classList.add('test');
329
267
  });
330
268
 
331
- // Initialize Intersection Observer for lazy loading
332
- this.initLazyLoading();
333
-
334
269
  this.$socket.on('widget-load:' + this.id, (msg) => {
335
270
  this.init(msg);
336
271
  });
@@ -345,192 +280,86 @@ export default {
345
280
  /* Make sure, any events you subscribe to on SocketIO are unsubscribed to here */
346
281
  this.$socket?.off('widget-load' + this.id);
347
282
  this.$socket?.off('msg-input:' + this.id);
348
-
349
- // Clean up Intersection Observer
350
- if (this.intersectionObserver) {
351
- this.intersectionObserver.disconnect();
352
- }
353
283
  },
354
284
  methods: {
355
- // Simplified component getter - now just returns from computed cache
356
- getFieldComponent(field) {
357
- return this.fieldComponents[field.id] || this.createComponent(field);
358
- },
359
-
360
- // Clear cache when form data changes
361
- clearComponentCache() {
362
- // This is now handled by computed properties automatically
363
- },
364
-
365
- // Safe method to get field hint for template use
366
- getFieldHint(field) {
367
- try {
368
- if (field.customForm) {
369
- let customForm;
370
- if (typeof field.customForm === 'string') {
371
- customForm = JSON.parse(field.customForm);
372
- } else if (typeof field.customForm === 'object') {
373
- customForm = field.customForm;
374
- }
375
- return customForm?.hint;
376
- }
377
- } catch (error) {
378
- console.warn('Failed to parse customForm hint for field', field.id, error);
379
- }
380
- return undefined;
381
- },
382
-
383
285
  createComponent(field) {
384
- // Safe parsing of customForm - handle both string and object cases
385
- let customForm = {};
386
- try {
387
- if (field.customForm) {
388
- if (typeof field.customForm === 'string') {
389
- customForm = JSON.parse(field.customForm);
390
- } else if (typeof field.customForm === 'object') {
391
- customForm = field.customForm;
392
- }
393
- }
394
- } catch (error) {
395
- console.warn('Failed to parse customForm for field', field.id, error);
396
- customForm = {};
397
- }
398
- const { hint, placeholder, validation, customProperties = [] } = customForm;
286
+ console.debug('Creating component for field:', field);
287
+ const customForm = field.customForm ? JSON.parse(field.customForm) : {};
288
+ const hint = customForm.hint;
289
+ const placeholder = customForm.placeholder;
290
+ const validation = customForm.validation;
399
291
  const name = field.id;
292
+ const customProperties = customForm.customProperties ?? [];
400
293
  const isReadOnly =
401
294
  this.props.readonly ||
402
- this.formIsFinished ||
403
- customProperties.some(
404
- (entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true'
405
- )
295
+ this.formIsFinished ||
296
+ customProperties.find((entry) => ['readOnly', 'readonly'].includes(entry.name) && entry.value === 'true')
406
297
  ? 'true'
407
298
  : undefined;
408
-
409
- const commonFormKitProps = {
410
- id: field.id,
411
- name,
412
- label: field.label,
413
- required: field.required,
414
- value: this.formData[field.id],
415
- help: hint,
416
- wrapperClass: '$remove:formkit-wrapper',
417
- labelClass: 'ui-dynamic-form-input-label',
418
- inputClass: `input-${this.theme}`,
419
- innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
420
- readonly: isReadOnly,
421
- validation,
422
- validationVisibility: 'live',
423
- };
424
-
425
299
  switch (field.type) {
426
300
  case 'long':
427
301
  return {
428
302
  type: 'FormKit',
429
303
  props: {
430
- ...commonFormKitProps,
431
304
  type: 'number',
305
+ id: field.id,
306
+ name,
307
+ label: field.label,
308
+ required: field.required,
309
+ value: this.formData[field.id],
432
310
  number: 'integer',
433
311
  min: 0,
434
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',
435
320
  },
436
321
  };
437
322
  case 'number':
438
- const step = customForm.step;
323
+ const step = field.customForm ? JSON.parse(field.customForm).step : undefined;
439
324
  return {
440
325
  type: 'FormKit',
441
326
  props: {
442
- ...commonFormKitProps,
443
327
  type: 'number',
328
+ id: field.id,
329
+ name,
330
+ label: field.label,
331
+ required: field.required,
332
+ value: this.formData[field.id],
444
333
  step,
445
334
  number: 'float',
446
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',
447
343
  },
448
344
  };
449
345
  case 'date':
450
346
  return {
451
347
  type: 'FormKit',
452
348
  props: {
453
- ...commonFormKitProps,
454
349
  type: 'date',
455
- },
456
- };
457
- case 'string':
458
- return {
459
- type: 'FormKit',
460
- props: {
461
- ...commonFormKitProps,
462
- type: 'text',
463
- placeholder,
464
- },
465
- };
466
- case 'email':
467
- return {
468
- type: 'FormKit',
469
- props: {
470
- ...commonFormKitProps,
471
- type: 'email',
472
- placeholder,
473
- },
474
- };
475
- case 'password':
476
- return {
477
- type: 'FormKit',
478
- props: {
479
- ...commonFormKitProps,
480
- type: 'password',
481
- placeholder,
482
- },
483
- };
484
- case 'tel':
485
- return {
486
- type: 'FormKit',
487
- props: {
488
- ...commonFormKitProps,
489
- type: 'tel',
490
- placeholder,
491
- },
492
- };
493
- case 'url':
494
- return {
495
- type: 'FormKit',
496
- props: {
497
- ...commonFormKitProps,
498
- type: 'url',
499
- placeholder,
500
- },
501
- };
502
- case 'time':
503
- return {
504
- type: 'FormKit',
505
- props: {
506
- ...commonFormKitProps,
507
- type: 'time',
508
- placeholder,
509
- },
510
- };
511
- case 'week':
512
- return {
513
- type: 'FormKit',
514
- props: {
515
- ...commonFormKitProps,
516
- type: 'week',
517
- placeholder,
518
- },
519
- };
520
- case 'month':
521
- return {
522
- type: 'FormKit',
523
- props: {
524
- ...commonFormKitProps,
525
- type: 'month',
526
- },
527
- };
528
- case 'datetime-local':
529
- return {
530
- type: 'FormKit',
531
- props: {
532
- ...commonFormKitProps,
533
- 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',
534
363
  },
535
364
  };
536
365
  case 'enum':
@@ -551,9 +380,7 @@ export default {
551
380
  wrapperClass: '$remove:formkit-wrapper',
552
381
  labelClass: 'ui-dynamic-form-input-label',
553
382
  inputClass: `input-${this.theme}`,
554
- innerClass: `ui-dynamic-form-input-outlines ${
555
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
556
- }`,
383
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
557
384
  readonly: isReadOnly,
558
385
  disabled: isReadOnly,
559
386
  validation,
@@ -561,11 +388,9 @@ export default {
561
388
  },
562
389
  };
563
390
  case 'select':
564
- const selections = customForm.entries
565
- ? customForm.entries.map((obj) => {
566
- return { value: obj.key, label: obj.value };
567
- })
568
- : [];
391
+ const selections = JSON.parse(field.customForm).entries.map((obj) => {
392
+ return { value: obj.key, label: obj.value };
393
+ });
569
394
  return {
570
395
  type: 'FormKit',
571
396
  props: {
@@ -581,9 +406,7 @@ export default {
581
406
  wrapperClass: '$remove:formkit-wrapper',
582
407
  labelClass: 'ui-dynamic-form-input-label',
583
408
  inputClass: `input-${this.theme}`,
584
- innerClass: `ui-dynamic-form-input-outlines ${
585
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
586
- }`,
409
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
587
410
  readonly: isReadOnly,
588
411
  disabled: isReadOnly,
589
412
  validation,
@@ -605,9 +428,7 @@ export default {
605
428
  wrapperClass: '$remove:formkit-wrapper',
606
429
  labelClass: 'ui-dynamic-form-input-label',
607
430
  inputClass: `input-${this.theme}`,
608
- innerClass: `ui-dynamic-form-input-outlines ${
609
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
610
- }`,
431
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
611
432
  readonly: isReadOnly,
612
433
  validation,
613
434
  validationVisibility: 'live',
@@ -631,86 +452,15 @@ export default {
631
452
  help: hint,
632
453
  labelClass: 'ui-dynamic-form-input-label',
633
454
  inputClass: `input-${this.theme}`,
634
- innerClass: `ui-dynamic-form-input-outlines ${
635
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
636
- }`,
455
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
637
456
  readonly: isReadOnly,
638
457
  disabled: isReadOnly,
639
458
  validation,
640
459
  validationVisibility: 'live',
641
460
  },
642
461
  };
643
- case 'file-preview':
644
- // Handle file preview display only (no upload functionality)
645
- const originalFieldId = field.id.replace('_preview', '');
646
- if (this.formData && this.formData[originalFieldId] && this.formData[originalFieldId].length != 0) {
647
- const fileDataArray = Array.isArray(this.formData[originalFieldId])
648
- ? this.formData[originalFieldId]
649
- : [this.formData[originalFieldId]];
650
-
651
- // Create unique container ID for this field
652
- const containerId = `file-preview-${field.id}`;
653
-
654
- // Check if this field is already visible (for immediate processing)
655
- if (this.visibleFileFields.has(field.id)) {
656
- // Return loading state initially
657
- const loadingContent = `
658
- <div id="${containerId}" data-lazy-field="${field.id}">
659
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
660
- field.required ? ' *' : ''
661
- }</label>
662
- <div style="margin-top: 8px; padding: 20px; text-align: center; color: #666;">
663
- <div style="font-size: 1.2em; margin-bottom: 8px;">⏳</div>
664
- <div>Dateien werden geladen...</div>
665
- </div>
666
- </div>
667
- `;
668
-
669
- // Process files asynchronously
670
- setTimeout(() => {
671
- this.processFilePreview(containerId, fileDataArray, field);
672
- }, 0);
673
-
674
- return {
675
- type: 'div',
676
- props: {
677
- innerHTML: loadingContent,
678
- },
679
- };
680
- } else {
681
- // Return lazy loading placeholder
682
- const lazyContent = `
683
- <div id="${containerId}" data-lazy-field="${field.id}" class="lazy-file-preview">
684
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
685
- field.required ? ' *' : ''
686
- }</label>
687
- <div style="margin-top: 8px; padding: 40px; text-align: center; color: #999; border: 1px dashed #ddd; border-radius: 4px;">
688
- <div style="font-size: 1.5em; margin-bottom: 12px;">📁</div>
689
- <div>Dateien werden geladen, wenn sie sichtbar werden...</div>
690
- <div style="margin-top: 8px; font-size: 0.9em;">${
691
- fileDataArray.length
692
- } Datei(en)</div>
693
- </div>
694
- </div>
695
- `;
696
-
697
- return {
698
- type: 'div',
699
- props: {
700
- innerHTML: lazyContent,
701
- },
702
- };
703
- }
704
- }
705
- // If no files to preview, return empty div
706
- return {
707
- type: 'div',
708
- props: {
709
- style: 'display: none;',
710
- },
711
- };
712
462
  case 'file':
713
- const multiple = customForm.multiple === 'true';
463
+ const multiple = field.customForm ? JSON.parse(field.customForm).multiple === 'true' : false;
714
464
  return {
715
465
  type: 'FormKit',
716
466
  props: {
@@ -719,11 +469,13 @@ export default {
719
469
  name,
720
470
  label: field.label,
721
471
  required: field.required,
472
+ value: this.formData[field.id],
722
473
  help: hint,
723
474
  innerClass: 'reset-background',
724
475
  wrapperClass: '$remove:formkit-wrapper',
725
476
  labelClass: 'ui-dynamic-form-input-label',
726
477
  inputClass: `input-${this.theme}`,
478
+ // innerClass: ui-dynamic-form-input-outlines `${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
727
479
  readonly: isReadOnly,
728
480
  disabled: isReadOnly,
729
481
  multiple,
@@ -732,11 +484,9 @@ export default {
732
484
  },
733
485
  };
734
486
  case 'checkbox':
735
- const options = customForm.entries
736
- ? customForm.entries.map((obj) => {
737
- return { value: obj.key, label: obj.value };
738
- })
739
- : [];
487
+ const options = JSON.parse(field.customForm).entries.map((obj) => {
488
+ return { value: obj.key, label: obj.value };
489
+ });
740
490
  return {
741
491
  type: 'FormKit',
742
492
  props: {
@@ -751,9 +501,7 @@ export default {
751
501
  fieldsetClass: 'custom-fieldset',
752
502
  labelClass: 'ui-dynamic-form-input-label',
753
503
  inputClass: `input-${this.theme}`,
754
- innerClass: `ui-dynamic-form-input-outlines ${
755
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
756
- }`,
504
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
757
505
  readonly: isReadOnly,
758
506
  disabled: isReadOnly,
759
507
  validation,
@@ -791,9 +539,7 @@ export default {
791
539
  wrapperClass: '$remove:formkit-wrapper',
792
540
  labelClass: 'ui-dynamic-form-input-label',
793
541
  inputClass: `input-${this.theme}`,
794
- innerClass: `ui-dynamic-form-input-outlines ${
795
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
796
- }`,
542
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
797
543
  readonly: isReadOnly,
798
544
  validation,
799
545
  validationVisibility: 'live',
@@ -814,9 +560,7 @@ export default {
814
560
  wrapperClass: '$remove:formkit-wrapper',
815
561
  labelClass: 'ui-dynamic-form-input-label',
816
562
  inputClass: `input-${this.theme}`,
817
- innerClass: `ui-dynamic-form-input-outlines ${
818
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
819
- }`,
563
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
820
564
  readonly: isReadOnly,
821
565
  validation,
822
566
  validationVisibility: 'live',
@@ -824,10 +568,10 @@ export default {
824
568
  };
825
569
  case 'header':
826
570
  let typeToUse = 'h1';
827
- if (customForm.style === 'heading_2') {
571
+ if (field.customForm && JSON.parse(field.customForm).style === 'heading_2') {
828
572
  typeToUse = 'h2';
829
573
  }
830
- if (customForm.style === 'heading_3') {
574
+ if (field.customForm && JSON.parse(field.customForm).style === 'heading_3') {
831
575
  typeToUse = 'h3';
832
576
  }
833
577
  return {
@@ -856,9 +600,7 @@ export default {
856
600
  wrapperClass: '$remove:formkit-wrapper',
857
601
  labelClass: 'ui-dynamic-form-input-label',
858
602
  inputClass: `input-${this.theme}`,
859
- innerClass: `ui-dynamic-form-input-outlines ${
860
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
861
- }`,
603
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
862
604
  readonly: isReadOnly,
863
605
  validation,
864
606
  validationVisibility: 'live',
@@ -887,20 +629,16 @@ export default {
887
629
  wrapperClass: '$remove:formkit-wrapper',
888
630
  labelClass: 'ui-dynamic-form-input-label',
889
631
  inputClass: `input-${this.theme}`,
890
- innerClass: `ui-dynamic-form-input-outlines ${
891
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
892
- }`,
632
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
893
633
  readonly: isReadOnly,
894
634
  validation,
895
635
  validationVisibility: 'live',
896
636
  },
897
637
  };
898
638
  case 'radio':
899
- const radioOptions = customForm.entries
900
- ? customForm.entries.map((obj) => {
901
- return { value: obj.key, label: obj.value };
902
- })
903
- : [];
639
+ const radioOptions = JSON.parse(field.customForm).entries.map((obj) => {
640
+ return { value: obj.key, label: obj.value };
641
+ });
904
642
  return {
905
643
  type: 'FormKit',
906
644
  props: {
@@ -915,9 +653,7 @@ export default {
915
653
  fieldsetClass: 'custom-fieldset',
916
654
  labelClass: 'ui-dynamic-form-input-label',
917
655
  inputClass: `input-${this.theme}`,
918
- innerClass: `ui-dynamic-form-input-outlines ${
919
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
920
- }`,
656
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
921
657
  readonly: isReadOnly,
922
658
  disabled: isReadOnly,
923
659
  validation,
@@ -925,6 +661,7 @@ export default {
925
661
  },
926
662
  };
927
663
  case 'range':
664
+ const customForm = JSON.parse(field.customForm);
928
665
  return {
929
666
  type: 'v-slider',
930
667
  props: {
@@ -963,16 +700,14 @@ export default {
963
700
  wrapperClass: '$remove:formkit-wrapper',
964
701
  labelClass: 'ui-dynamic-form-input-label',
965
702
  inputClass: `input-${this.theme}`,
966
- innerClass: `ui-dynamic-form-input-outlines ${
967
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
968
- }`,
703
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
969
704
  readonly: isReadOnly,
970
705
  validation,
971
706
  validationVisibility: 'live',
972
707
  },
973
708
  };
974
709
  case 'textarea':
975
- const rows = customForm.rows;
710
+ const rows = field.customForm ? JSON.parse(field.customForm).rows : undefined;
976
711
  return {
977
712
  type: 'FormKit',
978
713
  props: {
@@ -988,9 +723,7 @@ export default {
988
723
  wrapperClass: '$remove:formkit-wrapper',
989
724
  labelClass: 'ui-dynamic-form-input-label',
990
725
  inputClass: `input-${this.theme}`,
991
- innerClass: `ui-dynamic-form-input-outlines ${
992
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
993
- }`,
726
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
994
727
  readonly: isReadOnly,
995
728
  validation,
996
729
  validationVisibility: 'live',
@@ -1011,9 +744,7 @@ export default {
1011
744
  wrapperClass: '$remove:formkit-wrapper',
1012
745
  labelClass: 'ui-dynamic-form-input-label',
1013
746
  inputClass: `input-${this.theme}`,
1014
- innerClass: `ui-dynamic-form-input-outlines ${
1015
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1016
- }`,
747
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1017
748
  readonly: isReadOnly,
1018
749
  validation,
1019
750
  validationVisibility: 'live',
@@ -1034,9 +765,7 @@ export default {
1034
765
  wrapperClass: '$remove:formkit-wrapper',
1035
766
  labelClass: 'ui-dynamic-form-input-label',
1036
767
  inputClass: `input-${this.theme}`,
1037
- innerClass: `ui-dynamic-form-input-outlines ${
1038
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1039
- }`,
768
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1040
769
  readonly: isReadOnly,
1041
770
  validation,
1042
771
  validationVisibility: 'live',
@@ -1057,9 +786,7 @@ export default {
1057
786
  wrapperClass: '$remove:formkit-wrapper',
1058
787
  labelClass: 'ui-dynamic-form-input-label',
1059
788
  inputClass: `input-${this.theme}`,
1060
- innerClass: `ui-dynamic-form-input-outlines ${
1061
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1062
- }`,
789
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1063
790
  readonly: isReadOnly,
1064
791
  validation,
1065
792
  validationVisibility: 'live',
@@ -1078,9 +805,7 @@ export default {
1078
805
  help: hint,
1079
806
  labelClass: 'ui-dynamic-form-input-label',
1080
807
  inputClass: `input-${this.theme}`,
1081
- innerClass: `ui-dynamic-form-input-outlines ${
1082
- this.theme === 'dark' ? '$remove:formkit-inner' : ''
1083
- }`,
808
+ innerClass: `ui-dynamic-form-input-outlines ${this.theme === 'dark' ? '$remove:formkit-inner' : ''}`,
1084
809
  readonly: isReadOnly,
1085
810
  validation,
1086
811
  validationVisibility: 'live',
@@ -1088,220 +813,6 @@ export default {
1088
813
  };
1089
814
  }
1090
815
  },
1091
- initLazyLoading() {
1092
- // Initialize Intersection Observer for lazy loading of file previews
1093
- if (typeof IntersectionObserver !== 'undefined') {
1094
- this.intersectionObserver = new IntersectionObserver(
1095
- (entries) => {
1096
- entries.forEach((entry) => {
1097
- if (entry.isIntersecting) {
1098
- const element = entry.target;
1099
- const fieldId = element.getAttribute('data-lazy-field');
1100
-
1101
- if (fieldId && !this.visibleFileFields.has(fieldId)) {
1102
- this.visibleFileFields.add(fieldId);
1103
- this.loadFilePreview(fieldId);
1104
- this.intersectionObserver.unobserve(element);
1105
- }
1106
- }
1107
- });
1108
- },
1109
- {
1110
- root: null,
1111
- rootMargin: '50px', // Start loading 50px before element becomes visible
1112
- threshold: 0.1,
1113
- }
1114
- );
1115
-
1116
- // Observe all lazy file preview elements
1117
- this.$nextTick(() => {
1118
- this.observeLazyElements();
1119
- });
1120
- } else {
1121
- // Fallback for browsers without Intersection Observer
1122
- // Load all file previews immediately
1123
- this.loadAllFilePreviews();
1124
- }
1125
- },
1126
- observeLazyElements() {
1127
- const lazyElements = document.querySelectorAll('.lazy-file-preview[data-lazy-field]');
1128
- lazyElements.forEach((element) => {
1129
- if (this.intersectionObserver) {
1130
- this.intersectionObserver.observe(element);
1131
- }
1132
- });
1133
- },
1134
- loadFilePreview(fieldId) {
1135
- // Find the field configuration
1136
- const field = this.userTask?.userTaskConfig?.formFields?.find((f) => f.id === fieldId);
1137
- if (!field) return;
1138
-
1139
- const originalFieldId = fieldId.replace('_preview', '');
1140
- const fileDataArray = this.formData[originalFieldId];
1141
-
1142
- if (!fileDataArray || fileDataArray.length === 0) return;
1143
-
1144
- const containerId = `file-preview-${fieldId}`;
1145
- const container = document.getElementById(containerId);
1146
-
1147
- if (container) {
1148
- // Show loading state
1149
- container.innerHTML = `
1150
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
1151
- field.required ? ' *' : ''
1152
- }</label>
1153
- <div style="margin-top: 8px; padding: 20px; text-align: center; color: #666;">
1154
- <div style="font-size: 1.2em; margin-bottom: 8px;">⏳</div>
1155
- <div>Dateien werden geladen...</div>
1156
- </div>
1157
- `;
1158
-
1159
- // Process files
1160
- setTimeout(() => {
1161
- this.processFilePreview(containerId, fileDataArray, field);
1162
- }, 0);
1163
- }
1164
- },
1165
- loadAllFilePreviews() {
1166
- // Fallback method - load all file previews immediately
1167
- const fileFields =
1168
- this.userTask?.userTaskConfig?.formFields?.filter((f) => f.type === 'file-preview') || [];
1169
- fileFields.forEach((field) => {
1170
- if (!this.visibleFileFields.has(field.id)) {
1171
- this.visibleFileFields.add(field.id);
1172
- this.loadFilePreview(field.id);
1173
- }
1174
- });
1175
- },
1176
- processFilePreview(containerId, fileDataArray, field) {
1177
- // Process files in chunks to avoid blocking the UI
1178
- const processInChunks = async () => {
1179
- const images = [];
1180
- const otherFiles = [];
1181
-
1182
- // Process files in batches to avoid UI blocking
1183
- const batchSize = 3;
1184
-
1185
- for (let i = 0; i < fileDataArray.length; i += batchSize) {
1186
- const batch = fileDataArray.slice(i, i + batchSize);
1187
-
1188
- for (const fileData of batch) {
1189
- const fileName = fileData.name || '';
1190
- const isImage = fileName.toLowerCase().match(/\.(png|jpg|jpeg|gif|webp)$/);
1191
-
1192
- if (isImage && fileData.file && fileData.file.data) {
1193
- // Convert buffer to base64 data URL for image display
1194
- const uint8Array = new Uint8Array(fileData.file.data);
1195
- let binaryString = '';
1196
-
1197
- // Process in chunks to avoid call stack overflow
1198
- const chunkSize = 1024;
1199
- for (let j = 0; j < uint8Array.length; j += chunkSize) {
1200
- const chunk = uint8Array.slice(j, j + chunkSize);
1201
- binaryString += String.fromCharCode.apply(null, chunk);
1202
- }
1203
-
1204
- const base64String = btoa(binaryString);
1205
- const mimeType = fileName.toLowerCase().endsWith('.png')
1206
- ? 'image/png'
1207
- : fileName.toLowerCase().endsWith('.gif')
1208
- ? 'image/gif'
1209
- : 'image/jpeg';
1210
- const dataURL = `data:${mimeType};base64,${base64String}`;
1211
-
1212
- images.push({ fileName, dataURL, fileData });
1213
- } else {
1214
- otherFiles.push({ fileName, fileData });
1215
- }
1216
- }
1217
-
1218
- // Allow UI to update between batches
1219
- await new Promise((resolve) => setTimeout(resolve, 10));
1220
- }
1221
-
1222
- // Build the final content
1223
- let content = `<label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
1224
- field.required ? ' *' : ''
1225
- }</label>`;
1226
-
1227
- // Display images
1228
- if (images.length > 0) {
1229
- content += '<div style="margin-top: 8px;">';
1230
- content += '<div style="font-weight: bold; margin-bottom: 8px;">Bilder:</div>';
1231
- images.forEach((img, index) => {
1232
- const downloadId = `download-img-${field.id}-${index}`;
1233
- content += `
1234
- <div style="display: inline-block; margin: 8px; text-align: center; vertical-align: top;">
1235
- <img src="${img.dataURL}" alt="${img.fileName}"
1236
- style="max-width: 300px; max-height: 200px; border: 1px solid #ccc; display: block; cursor: pointer;"
1237
- onclick="document.getElementById('${downloadId}').click();" />
1238
- <div style="margin-top: 4px; font-size: 0.9em; color: #666; max-width: 300px; word-break: break-word;">
1239
- ${img.fileName}
1240
- </div>
1241
- <a id="${downloadId}" href="${img.dataURL}" download="${img.fileName}" style="display: none;"></a>
1242
- </div>
1243
- `;
1244
- });
1245
- content += '</div>';
1246
- }
1247
-
1248
- // Display other files as list
1249
- if (otherFiles.length > 0) {
1250
- content +=
1251
- '<div style="margin-top: 12px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">';
1252
- content += '<div style="font-weight: bold; margin-bottom: 8px;">Weitere Dateien:</div>';
1253
- otherFiles.forEach((file, index) => {
1254
- const downloadId = `download-file-${field.id}-${index}`;
1255
- const uint8Array = new Uint8Array(file.fileData.file.data);
1256
- let binaryString = '';
1257
-
1258
- // Process in chunks for download
1259
- const chunkSize = 1024;
1260
- for (let j = 0; j < uint8Array.length; j += chunkSize) {
1261
- const chunk = uint8Array.slice(j, j + chunkSize);
1262
- binaryString += String.fromCharCode.apply(null, chunk);
1263
- }
1264
-
1265
- const base64String = btoa(binaryString);
1266
- const dataURL = `data:application/octet-stream;base64,${base64String}`;
1267
-
1268
- content += `
1269
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px; padding: 4px; border-radius: 3px; cursor: pointer;"
1270
- onclick="document.getElementById('${downloadId}').click();"
1271
- onmouseover="this.style.backgroundColor='#e6e6e6';"
1272
- onmouseout="this.style.backgroundColor='transparent';">
1273
- <span style="font-size: 1.2em;">📎</span>
1274
- <span style="flex: 1; word-break: break-word;">${file.fileName}</span>
1275
- <span style="font-size: 0.8em; color: #007bff;">Download</span>
1276
- <a id="${downloadId}" href="${dataURL}" download="${file.fileName}" style="display: none;"></a>
1277
- </div>
1278
- `;
1279
- });
1280
- content += '</div>';
1281
- }
1282
-
1283
- // Update the container with the final content
1284
- const container = document.getElementById(containerId);
1285
- if (container) {
1286
- container.innerHTML = content;
1287
- }
1288
- };
1289
-
1290
- processInChunks().catch((error) => {
1291
- const container = document.getElementById(containerId);
1292
- if (container) {
1293
- container.innerHTML = `
1294
- <label class="ui-dynamic-form-input-label">${field.label} (Vorschau)${
1295
- field.required ? ' *' : ''
1296
- }</label>
1297
- <div style="margin-top: 8px; padding: 20px; text-align: center; color: #d32f2f;">
1298
- <div style="font-size: 1.2em; margin-bottom: 8px;">⚠️</div>
1299
- <div>Fehler beim Laden der Dateien</div>
1300
- </div>
1301
- `;
1302
- }
1303
- });
1304
- },
1305
816
  toggleCollapse() {
1306
817
  this.collapsed = !this.collapsed;
1307
818
  },
@@ -1318,7 +829,13 @@ export default {
1318
829
  return style;
1319
830
  },
1320
831
  fields() {
1321
- 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;
1322
839
  },
1323
840
  /*
1324
841
  widget-action just sends a msg to Node-RED, it does not store the msg state server-side
@@ -1331,8 +848,6 @@ export default {
1331
848
  },
1332
849
  init(msg) {
1333
850
  this.msg = msg;
1334
- this.clearComponentCache();
1335
-
1336
851
  if (!msg) {
1337
852
  return;
1338
853
  }
@@ -1346,8 +861,6 @@ export default {
1346
861
  } else {
1347
862
  this.userTask = null;
1348
863
  this.formData = {};
1349
- // Reset lazy loading state
1350
- this.visibleFileFields.clear();
1351
864
  return;
1352
865
  }
1353
866
 
@@ -1360,27 +873,12 @@ export default {
1360
873
  this.collapsed = this.props.collapse_when_finished;
1361
874
  }
1362
875
 
1363
- // Reset lazy loading state for new task
1364
- this.visibleFileFields.clear();
1365
-
1366
876
  if (formFields) {
1367
877
  formFields.forEach((field) => {
1368
878
  this.formData[field.id] = field.defaultValue;
1369
879
 
1370
880
  if (field.type === 'confirm') {
1371
- let customForm = {};
1372
- try {
1373
- if (field.customForm) {
1374
- if (typeof field.customForm === 'string') {
1375
- customForm = JSON.parse(field.customForm);
1376
- } else if (typeof field.customForm === 'object') {
1377
- customForm = field.customForm;
1378
- }
1379
- }
1380
- } catch (error) {
1381
- console.warn('Failed to parse customForm for confirm field', field.id, error);
1382
- customForm = {};
1383
- }
881
+ const customForm = field.customForm ? JSON.parse(field.customForm) : {};
1384
882
  const confirmText = customForm.confirmButtonText ?? 'Confirm';
1385
883
  const declineText = customForm.declineButtonText ?? 'Decline';
1386
884
  this.actions = [
@@ -1399,18 +897,6 @@ export default {
1399
897
  ];
1400
898
  }
1401
899
  });
1402
-
1403
- // Check for file fields and duplicate them as file-preview if initial values exist
1404
- // Insert preview fields directly before their corresponding file fields
1405
- for (let i = formFields.length - 1; i >= 0; i--) {
1406
- const field = formFields[i];
1407
- if (field.type === 'file' && initialValues && initialValues[field.id]) {
1408
- const previewField = { ...field };
1409
- previewField.type = 'file-preview';
1410
- previewField.id = `${field.id}_preview`; // Give it a unique ID
1411
- this.userTask.userTaskConfig.formFields.splice(i, 0, previewField);
1412
- }
1413
- }
1414
900
  }
1415
901
 
1416
902
  if (initialValues) {
@@ -1429,11 +915,8 @@ export default {
1429
915
  });
1430
916
  }
1431
917
 
1432
- // Force update of computed properties by triggering reactivity
1433
- this.$nextTick(() => {
918
+ nextTick(() => {
1434
919
  this.focusFirstFormField();
1435
- // Re-observe lazy elements after DOM update
1436
- this.observeLazyElements();
1437
920
  });
1438
921
  },
1439
922
  actionFn(action) {
@@ -1484,7 +967,7 @@ export default {
1484
967
  this.send(
1485
968
  msg,
1486
969
  this.actions.findIndex((element) => element.label === action.label) +
1487
- (this.isConfirmDialog ? this.props.options.length : 0)
970
+ (this.isConfirmDialog ? this.props.options.length : 0)
1488
971
  );
1489
972
  // TODO: mm - end
1490
973
  } else {
@@ -1499,6 +982,7 @@ export default {
1499
982
  const result = func(this.formData, this.userTask, this.msg);
1500
983
  return Boolean(result);
1501
984
  } catch (err) {
985
+ console.error('Error while evaluating condition: ' + err);
1502
986
  return false;
1503
987
  }
1504
988
  },
@@ -1519,14 +1003,14 @@ export default {
1519
1003
  if (['INPUT', 'TEXTAREA', 'SELECT'].includes(this.firstFormFieldRef.$el.tagName)) {
1520
1004
  inputElement = this.firstFormFieldRef.$el;
1521
1005
  } else {
1522
- inputElement = this.firstFormFieldRef.$el.querySelector(
1523
- 'input:not([type="hidden"]), textarea, select'
1524
- );
1006
+ inputElement = this.firstFormFieldRef.$el.querySelector('input:not([type="hidden"]), textarea, select');
1525
1007
  }
1526
1008
  }
1527
1009
 
1528
1010
  if (inputElement) {
1529
1011
  inputElement.focus();
1012
+ } else {
1013
+ console.warn('Could not find a focusable input element for the first form field.');
1530
1014
  }
1531
1015
  }
1532
1016
  },
@@ -1548,29 +1032,4 @@ function mapItems(type, field) {
1548
1032
  <style>
1549
1033
  /* CSS is auto scoped, but using named classes is still recommended */
1550
1034
  @import '../stylesheets/ui-dynamic-form.css';
1551
-
1552
- /* Lazy loading styles */
1553
- .lazy-file-preview {
1554
- transition: opacity 0.3s ease-in-out;
1555
- }
1556
-
1557
- .lazy-file-preview .lazy-placeholder {
1558
- background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
1559
- background-size: 200% 200%;
1560
- animation: shimmer 2s infinite;
1561
- }
1562
-
1563
- @keyframes shimmer {
1564
- 0% {
1565
- background-position: -200% -200%;
1566
- }
1567
- 100% {
1568
- background-position: 200% 200%;
1569
- }
1570
- }
1571
-
1572
- .lazy-file-preview:hover {
1573
- transform: translateY(-1px);
1574
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1575
- }
1576
1035
  </style>