viur-ui 0.0.1

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.
Files changed (114) hide show
  1. package/README.md +79 -0
  2. package/index.d.ts +351 -0
  3. package/index.js +46 -0
  4. package/package.json +84 -0
  5. package/src/components/Browser/Browser.vue +496 -0
  6. package/src/components/Browser/TreeNav.vue +408 -0
  7. package/src/components/Browser/ViurBrowser.d.vue.ts +64 -0
  8. package/src/components/Browser/ViurBrowser.vue +271 -0
  9. package/src/components/Browser/dragSession.ts +134 -0
  10. package/src/components/Browser/index.ts +14 -0
  11. package/src/components/Browser/types.ts +30 -0
  12. package/src/components/DataTable/ColumnFilterPanel.vue +225 -0
  13. package/src/components/DataTable/DataTable.vue +3155 -0
  14. package/src/components/DataTable/ViurDataTable.d.vue.ts +72 -0
  15. package/src/components/DataTable/ViurDataTable.vue +331 -0
  16. package/src/components/DataTable/columns.ts +400 -0
  17. package/src/components/DataTable/index.ts +60 -0
  18. package/src/components/DataTable/rows.ts +446 -0
  19. package/src/components/DataTable/types.ts +267 -0
  20. package/src/components/DataTable/useColumnLayout.ts +294 -0
  21. package/src/components/DataTable/useGridNavigation.ts +193 -0
  22. package/src/components/DataTable/useVirtualRows.ts +126 -0
  23. package/src/components/DataTable/viurColumns.ts +330 -0
  24. package/src/forms/ViForm.d.vue.ts +106 -0
  25. package/src/forms/ViForm.vue +592 -0
  26. package/src/forms/ViFormCategory.vue +57 -0
  27. package/src/forms/bones/Bone.vue +1023 -0
  28. package/src/forms/bones/BoneActions.vue +127 -0
  29. package/src/forms/bones/BoneLabel.vue +122 -0
  30. package/src/forms/bones/BoneMultipleList.vue +91 -0
  31. package/src/forms/bones/BoneMultipleWrapper.vue +105 -0
  32. package/src/forms/bones/BoneNestedWrapper.vue +155 -0
  33. package/src/forms/bones/BoneSingleEntry.vue +51 -0
  34. package/src/forms/bones/FilterBone.vue +694 -0
  35. package/src/forms/bones/actionbar/defaultBar.vue +98 -0
  36. package/src/forms/bones/actionbar/fileBar.vue +165 -0
  37. package/src/forms/bones/actionbar/relationalBar.vue +125 -0
  38. package/src/forms/bones/actions/ActionDialog.vue +70 -0
  39. package/src/forms/bones/actions/Debug.vue +106 -0
  40. package/src/forms/bones/actions/Delete.vue +13 -0
  41. package/src/forms/bones/actions/DescribeImage.vue +201 -0
  42. package/src/forms/bones/actions/Download.vue +27 -0
  43. package/src/forms/bones/actions/Placeholder.vue +14 -0
  44. package/src/forms/bones/actions/Translation.vue +238 -0
  45. package/src/forms/bones/actions/index.js +280 -0
  46. package/src/forms/bones/bone.d.ts +45 -0
  47. package/src/forms/bones/country_information.json +779 -0
  48. package/src/forms/bones/index.js +225 -0
  49. package/src/forms/bones/uploadAdapter.js +46 -0
  50. package/src/forms/bones/utils.js +284 -0
  51. package/src/forms/bones/view/base_item.vue +35 -0
  52. package/src/forms/bones/view/boneLogic.js +202 -0
  53. package/src/forms/bones/view/index.js +5 -0
  54. package/src/forms/bones/widgets/booleanBone.vue +83 -0
  55. package/src/forms/bones/widgets/booleanBoneChoose.vue +57 -0
  56. package/src/forms/bones/widgets/booleanBoneSelect.vue +58 -0
  57. package/src/forms/bones/widgets/codeBone.vue +214 -0
  58. package/src/forms/bones/widgets/colorBone.vue +75 -0
  59. package/src/forms/bones/widgets/dateBone.vue +114 -0
  60. package/src/forms/bones/widgets/emailBone.vue +100 -0
  61. package/src/forms/bones/widgets/fileBone.vue +273 -0
  62. package/src/forms/bones/widgets/jsonBone.vue +63 -0
  63. package/src/forms/bones/widgets/keyBone.vue +71 -0
  64. package/src/forms/bones/widgets/numericBone.vue +125 -0
  65. package/src/forms/bones/widgets/passwordBone.vue +215 -0
  66. package/src/forms/bones/widgets/phoneBone.vue +173 -0
  67. package/src/forms/bones/widgets/rawBone.vue +94 -0
  68. package/src/forms/bones/widgets/recordBone.vue +64 -0
  69. package/src/forms/bones/widgets/relationalBone.vue +164 -0
  70. package/src/forms/bones/widgets/relationalBoneSelect.vue +120 -0
  71. package/src/forms/bones/widgets/selectBone.vue +122 -0
  72. package/src/forms/bones/widgets/selectBoneChoose.vue +104 -0
  73. package/src/forms/bones/widgets/spatialBone.vue +111 -0
  74. package/src/forms/bones/widgets/stringBone.vue +139 -0
  75. package/src/forms/bones/widgets/textBone.vue +641 -0
  76. package/src/forms/install.d.ts +4 -0
  77. package/src/forms/install.js +43 -0
  78. package/src/forms/layouts/LayloutList2Columns.vue +57 -0
  79. package/src/forms/layouts/LayoutCategory.vue +38 -0
  80. package/src/forms/layouts/LayoutList.vue +31 -0
  81. package/src/forms/utils.js +601 -0
  82. package/src/public/assets/icons/fontawesome-manifest.json +8 -0
  83. package/src/public/assets/icons/regular/file.svg +1 -0
  84. package/src/public/assets/icons/solid/arrows-left-right.svg +1 -0
  85. package/src/public/assets/icons/solid/bug.svg +1 -0
  86. package/src/public/assets/icons/solid/check.svg +1 -0
  87. package/src/public/assets/icons/solid/circle-info.svg +1 -0
  88. package/src/public/assets/icons/solid/compress.svg +1 -0
  89. package/src/public/assets/icons/solid/download.svg +1 -0
  90. package/src/public/assets/icons/solid/expand.svg +1 -0
  91. package/src/public/assets/icons/solid/grip-vertical.svg +1 -0
  92. package/src/public/assets/icons/solid/plus.svg +1 -0
  93. package/src/public/assets/icons/solid/question.svg +1 -0
  94. package/src/public/assets/icons/solid/triangle-exclamation.svg +1 -0
  95. package/src/public/assets/icons/solid/upload.svg +1 -0
  96. package/src/public/assets/icons/solid/xmark.svg +1 -0
  97. package/src/request/cache.js +183 -0
  98. package/src/request/configStore.js +516 -0
  99. package/src/request/envelope.js +211 -0
  100. package/src/request/index.js +35 -0
  101. package/src/request/messageStore.js +201 -0
  102. package/src/request/request.js +752 -0
  103. package/src/request/skel.js +90 -0
  104. package/src/theme/colorStore.js +168 -0
  105. package/src/translations/de.js +178 -0
  106. package/src/translations/en.js +177 -0
  107. package/src/user/LoginScreen.d.vue.ts +54 -0
  108. package/src/user/LoginScreen.vue +627 -0
  109. package/src/user/google.js +75 -0
  110. package/src/user/index.js +3 -0
  111. package/src/user/store.d.ts +59 -0
  112. package/src/user/store.js +183 -0
  113. package/src/webawesomeCompat.css +72 -0
  114. package/src/webawesomeConfig.js +61 -0
@@ -0,0 +1,1023 @@
1
+ <template>
2
+ <div
3
+ class="bone"
4
+ :class="{
5
+ 'has-subbones': state.bonestructure['using'],
6
+ 'is-label-top': label === 'top',
7
+ 'is-label-hide': ['hide', 'placeholder'].includes(label),
8
+ [`bone-type-${state.bonestructure['type'].split('.')[0]}`]: true,
9
+ [`bone-name-${name}`]: true,
10
+ 'is-required': state.required,
11
+ }"
12
+ >
13
+ <bone-label
14
+ v-if="!['hide', 'placeholder'].includes(label)"
15
+ :name="name"
16
+ :debug="state.debug"
17
+ >
18
+ <span :class="{ 'bone-label-required': state.required }">{{ state.bonestructure["descr"] }}</span>
19
+ <span
20
+ v-if="state.required"
21
+ class="bone-label-required"
22
+ >
23
+ *
24
+ </span>
25
+ <wa-tooltip
26
+ v-if="state.hasTooltip && !showLabelInfo"
27
+ :for="state.labelTooltipTargetId"
28
+ :content="$t(state.bonestructure.params['tooltip'])"
29
+ placement="top-center"
30
+ ></wa-tooltip>
31
+ <div
32
+ v-if="state.hasTooltip && !showLabelInfo"
33
+ :id="state.labelTooltipTargetId"
34
+ class="bone-label-tooltip"
35
+ >
36
+ <wa-icon name="question"></wa-icon>
37
+ </div>
38
+ </bone-label>
39
+
40
+ <wa-alert
41
+ v-if="showLabelInfo && state.hasTooltip"
42
+ variant="brand"
43
+ open
44
+ class="bone-label-info"
45
+ >
46
+ <wa-icon
47
+ slot="icon"
48
+ name="circle-info"
49
+ ></wa-icon>
50
+ {{ state.bonestructure.params["tooltip"] }}
51
+ </wa-alert>
52
+ <wa-tooltip
53
+ class="bone-tooltip"
54
+ :for="state.boneTooltipTargetId"
55
+ :disabled="!state.showtooltip"
56
+ :content="state.bonestructure?.['descr']"
57
+ style="--show-delay: 1000"
58
+ ></wa-tooltip>
59
+
60
+ <div
61
+ :id="state.boneTooltipTargetId"
62
+ class="bone-inner wa-stack wa-gap-xs"
63
+ :class="{ 'is-required': state.required }"
64
+ >
65
+ <!-- Multilanguage: Tab per Sprache -->
66
+ <div
67
+ v-if="state.multilanguage"
68
+ class="bone-row wa-flank:end wa-gap-xs wa-align-items-start"
69
+ >
70
+ <wa-tab-group
71
+ class="bone-lang-tabs"
72
+ placement="bottom"
73
+ >
74
+ <template
75
+ v-for="lang in state.languages"
76
+ :key="name + '_' + lang + '_tab'"
77
+ >
78
+ <wa-tab
79
+ slot="nav"
80
+ :panel="'lang_' + lang"
81
+ :active="state.currentLanguage === lang"
82
+ :data-lang="lang"
83
+ @click="updateLanguage"
84
+ >
85
+ {{ $t(lang) }}
86
+ </wa-tab>
87
+ <wa-tab-panel
88
+ :name="'lang_' + lang"
89
+ :active="state.currentLanguage === lang"
90
+ >
91
+ <bone-multiple-list
92
+ :is="is"
93
+ v-if="state.multiple && !BoneHasMultipleHandling(state.bonestructure['type'])"
94
+ :name="name"
95
+ :lang="lang"
96
+ :entries="state.bonevalue?.[lang]"
97
+ :bonestructure="state.bonestructure"
98
+ :readonly="state.readonly"
99
+ :actionbar="state.actionbar"
100
+ />
101
+ <!-- Wert-Actions (Löschen, Download …) je Sprache am Eintrag -->
102
+ <bone-single-entry
103
+ :is="is"
104
+ v-else
105
+ :name="name"
106
+ :lang="lang"
107
+ :bonevalue="state.bonevalue?.[lang]"
108
+ :bonestructure="state.bonestructure"
109
+ :boneactions="false"
110
+ />
111
+ </wa-tab-panel>
112
+ </template>
113
+ </wa-tab-group>
114
+ <!-- Bone-Actions über alle Sprachen (Übersetzen …) -->
115
+ <div
116
+ v-if="state.boneactions && !state.multiple && state.hasOptionalActions"
117
+ class="wa-cluster wa-gap-xs"
118
+ >
119
+ <bone-actions
120
+ :value="state.bonevalue"
121
+ :name="name"
122
+ :index="null"
123
+ :bone="state.bonestructure"
124
+ scope="bone"
125
+ @change="updateValue"
126
+ ></bone-actions>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Einsprachig -->
131
+ <template v-else>
132
+ <bone-multiple-list
133
+ :is="is"
134
+ v-if="state.multiple && !BoneHasMultipleHandling(state.bonestructure['type'])"
135
+ :name="name"
136
+ :entries="state.bonevalue"
137
+ :bonestructure="state.bonestructure"
138
+ :readonly="state.readonly"
139
+ :actionbar="state.actionbar"
140
+ />
141
+ <bone-single-entry
142
+ :is="is"
143
+ v-else
144
+ :name="name"
145
+ :bonevalue="state.bonevalue"
146
+ :bonestructure="state.bonestructure"
147
+ :boneactions="state.boneactions"
148
+ :autofocus="autofocus"
149
+ />
150
+ </template>
151
+
152
+ <!-- Fehlermeldungen -->
153
+ <template v-if="errorStyle === 'default'">
154
+ <wa-callout
155
+ v-for="message in state.errorMessages"
156
+ :key="message"
157
+ open
158
+ :summary="$t('bone.errors_summary')"
159
+ variant="danger"
160
+ class="bone-error"
161
+ >
162
+ <wa-icon
163
+ slot="icon"
164
+ name="triangle-exclamation"
165
+ ></wa-icon>
166
+ <div class="bone-error">
167
+ {{ errorMessage(message) }}
168
+ </div>
169
+ </wa-callout>
170
+ </template>
171
+ <template v-else>
172
+ <div
173
+ v-for="message in state.errorMessages"
174
+ :key="message"
175
+ style="font-size: 0.8em; color: red"
176
+ >
177
+ {{ errorMessage(message) }}
178
+ </div>
179
+ </template>
180
+ </div>
181
+ </div>
182
+ </template>
183
+
184
+ <script setup>
185
+ /** @displayName Bone */
186
+ import { reactive, computed, onBeforeMount, provide, watch, watchEffect, inject, unref } from "vue"
187
+ import BoneLabel from "./BoneLabel.vue"
188
+ import { BoneHasMultipleHandling, getBoneActionbar } from "./index"
189
+ import rawBone from "./widgets/rawBone.vue"
190
+ import Utils, { buildBoneDomId } from "./utils"
191
+ import BoneActions from "./BoneActions.vue"
192
+ import { formConfiguredActions, resolveActions } from "./actions"
193
+ import BoneMultipleList from "./BoneMultipleList.vue"
194
+ import BoneSingleEntry from "./BoneSingleEntry.vue"
195
+ import { useI18n } from "vue-i18n"
196
+
197
+ /**
198
+ * @emits change - Historisches Relikt, wird nicht direkt genutzt
199
+ * @emits change-internal {Object} - Internes Event für ViForm: { name, value, lang, index, valid }
200
+ * @emits handleClick - Historisches Relikt, wird nicht direkt genutzt
201
+ */
202
+ const emit = defineEmits(["change", "change-internal", "handleClick"])
203
+
204
+ const props = defineProps({
205
+ is: {
206
+ type: Object,
207
+ default: rawBone, // Das zu rendernde Bone-Widget (z.B. StringBone), typischerweise von ViForm gesetzt
208
+ },
209
+ name: {
210
+ type: String,
211
+ required: true, // Bone-Identifier; muss einem Key in structure und skel entsprechen
212
+ },
213
+ languages: {
214
+ type: Array,
215
+ default: undefined, // Explizite Sprach-Liste; überschreibt bonestructure.languages
216
+ },
217
+ multiple: {
218
+ type: Boolean,
219
+ default: false, // Expliziter Multiple-Flag; überschreibt bonestructure.multiple
220
+ },
221
+ readonly: {
222
+ type: Boolean,
223
+ default: false, // Expliziter Readonly-Flag; überschreibt bonestructure.readonly
224
+ },
225
+ required: {
226
+ type: Boolean,
227
+ default: false, // Expliziter Required-Flag; überschreibt bonestructure.required
228
+ },
229
+ params: {
230
+ type: Object,
231
+ default: undefined, // Zusätzliche Parameter; überschreibt bonestructure.params
232
+ },
233
+ value: {
234
+ type: [Object, String, Number, Boolean, Array],
235
+ default: undefined, // Expliziter Wert; überschreibt skel[name] wenn gesetzt
236
+ },
237
+ structure: {
238
+ type: Object,
239
+ required: true, // Vollständige Skeleton-Struktur (alle Bone-Definitionen des Formulars)
240
+ },
241
+ skel: {
242
+ type: null,
243
+ required: true, // Skeleton-Datenobjekt mit den aktuellen Werten
244
+ },
245
+ errors: {
246
+ type: Array,
247
+ default: () => [], // Fehler-Array aus der Server-Antwort; Einträge mit passendem fieldPath werden angezeigt
248
+ },
249
+ label: {
250
+ type: String,
251
+ default: "normal", // Label-Modus: "normal" | "top" | "hide" | "placeholder"
252
+ validator(value, props) {
253
+ return ["normal", "top", "hide", "placeholder"].includes(value)
254
+ },
255
+ },
256
+ boneactions: {
257
+ type: Boolean,
258
+ default: true, // Aktiviert BoneActions (Defaults pro Bone-Type + bone.params.actions). Explizit false um Actions zu unterdrücken.
259
+ },
260
+ showLabelInfo: {
261
+ type: Boolean,
262
+ default: false, // Zeigt Tooltip-Inhalt als wa-alert statt als Tooltip
263
+ },
264
+ autofocus: {
265
+ type: Boolean,
266
+ default: false, // Gibt autofocus an das Bone-Widget weiter
267
+ },
268
+ defaultLanguage: {
269
+ type: String,
270
+ default: "de", // Standardmäßig aktive Sprache im Multilang-Tab
271
+ },
272
+ showBoneTooltip: {
273
+ type: Boolean,
274
+ default: false, // Erzwingt den Tooltip auf dem Input-Bereich
275
+ },
276
+ errorStyle: {
277
+ type: String,
278
+ default: "default", // Fehler-Darstellungsstil: "default" (wa-alert) | "decent" (kleiner roter Text)
279
+ validator(value, props) {
280
+ return ["default", "decent"].includes(value)
281
+ },
282
+ },
283
+ debug: {
284
+ type: Boolean,
285
+ default: false, // Aktiviert Debug-Modus: Bone-Name als Tooltip am Label
286
+ },
287
+ })
288
+ const { t } = useI18n()
289
+
290
+ const parentBonePath = inject("bonePath", [])
291
+ // Für die Sichtbarkeit der Relations-Auswahl (Action-`when` prüft darauf).
292
+ const relationSelector = inject("viurRelationSelector", null)
293
+ // Formular-weite Action-Konfiguration (ViForm `actions`-Prop).
294
+ const boneActionsConfig = inject("boneActionsConfig", null)
295
+ /** Vollständiger Bone-Pfad als Array für verschachtelte Bone-DOM-IDs. @returns {ComputedRef<string[]>} */
296
+ const bonePath = computed(() => [...(unref(parentBonePath) ?? []), props.name])
297
+ provide("bonePath", bonePath)
298
+
299
+ /**
300
+ * Prüft ob ein Objekt ausschließlich null-Werte oder leere Arrays enthält.
301
+ * @param {Object} obj - Das zu prüfende Objekt
302
+ * @returns {boolean}
303
+ */
304
+ function isObjectEmpty(obj) {
305
+ for (const [, value] of Object.entries(obj)) {
306
+ if (value !== null) {
307
+ if (Array.isArray(value) && value.length > 0) {
308
+ return false
309
+ } else if (!Array.isArray(value)) {
310
+ return false
311
+ }
312
+ }
313
+ }
314
+ return true
315
+ }
316
+
317
+ const state = reactive({
318
+ boneactions: computed(() => props.boneactions), // Ob BoneActions-Schaltflächen angezeigt werden
319
+
320
+ // Die Bone-Definition aus der Struktur (z.B. type, params, languages)
321
+ bonestructure: computed(() => {
322
+ return props.structure?.[props.name]
323
+ }),
324
+ bonevalue: null, // Aktueller Bone-Wert; wird aus skel initialisiert und bei Änderungen aktualisiert
325
+ multilanguage: computed(() => state.languages?.length && state.languages.length > 0), // Ob der Bone mehrsprachig ist (mindestens eine Sprache konfiguriert)
326
+ // Aktive Sprach-Liste: aus props.languages oder bonestructure.languages
327
+ languages: computed(() => {
328
+ if (props.languages) {
329
+ return props.languages
330
+ }
331
+ return state.bonestructure && Object.keys(state.bonestructure).includes("languages")
332
+ ? state.bonestructure["languages"]
333
+ : []
334
+ }),
335
+ // Readonly-Status: props.readonly hat Vorrang vor bonestructure.readonly
336
+ readonly: computed(() => {
337
+ if (props.readonly) {
338
+ return props.readonly
339
+ }
340
+ return state.bonestructure && Object.keys(state.bonestructure).includes("readonly")
341
+ ? state.bonestructure["readonly"]
342
+ : false
343
+ }),
344
+ // Required-Status: props.required hat Vorrang vor bonestructure.required
345
+ required: computed(() => {
346
+ if (props.required) {
347
+ return props.required
348
+ }
349
+ return state.bonestructure && Object.keys(state.bonestructure).includes("required")
350
+ ? state.bonestructure["required"]
351
+ : false
352
+ }),
353
+ // Ob ein Tooltip-Text in bonestructure.params vorhanden ist
354
+ hasTooltip: computed(() => {
355
+ // `params` kommt von viur-core immer als Objekt; fehlt es (eigener Renderer,
356
+ // handgebaute Struktur), darf das Formular davon nicht sterben.
357
+ return state.bonestructure && Object.keys(state.bonestructure["params"] ?? {}).includes("tooltip") ? true : false
358
+ }),
359
+ // Multiple-Status: props.multiple hat Vorrang vor bonestructure.multiple
360
+ multiple: computed(() => {
361
+ if (props.multiple) {
362
+ return props.multiple
363
+ }
364
+ return state.bonestructure && Object.keys(state.bonestructure).includes("multiple")
365
+ ? state.bonestructure["multiple"]
366
+ : false
367
+ }),
368
+ // Bone-Parameter: props.params hat Vorrang vor bonestructure.params
369
+ params: computed(() => {
370
+ if (props.params) {
371
+ return props.params
372
+ }
373
+ return state.bonestructure && Object.keys(state.bonestructure).includes("params")
374
+ ? state.bonestructure["params"]
375
+ : {}
376
+ }),
377
+ // Die Actionbar-Komponente passend zum Bone-Typ (z.B. für File, Relational)
378
+ actionbar: computed(() => {
379
+ return getBoneActionbar(state.bonestructure?.["type"])
380
+ }),
381
+ // Ob der Bone-Wert leer ist (null, leeres Array, leeres Objekt). Im Readonly-Modus immer false
382
+ isEmpty: computed(() => {
383
+ // Ignore the computation when the state is readonly
384
+ if (state.readonly) return false
385
+
386
+ // Check for null or undefined values
387
+ if (!state.bonevalue) return true
388
+
389
+ // Check if the value is an array with elements
390
+ if (Array.isArray(state.bonevalue) && state.bonevalue.length === 0) return true
391
+
392
+ // Check if the value is an object and not an array, then use helper function to check if it's empty
393
+ if (state.bonevalue === Object(state.bonevalue) && !Array.isArray(state.bonevalue))
394
+ return isObjectEmpty(state.bonevalue)
395
+
396
+ return false
397
+ }),
398
+ // Fehlermeldungen aus props.errors, gefiltert auf diesen Bone (fieldPath + severity)
399
+ errorMessages: computed(() => {
400
+ let errors = []
401
+ for (let error of props.errors) {
402
+ if (
403
+ error["fieldPath"][0] === props.name &&
404
+ (error["fieldPath"].length === 1 ||
405
+ ((state.multilanguage || state.multiple) && error["fieldPath"].length === 2) ||
406
+ (state.multilanguage && state.multiple && error["fieldPath"].length === 3)) &&
407
+ (error["severity"] > 2 || (state.required && (error["severity"] === 2 || error["severity"] === 0)))
408
+ ) {
409
+ //severity level???
410
+ errors.push(error["errorMessage"])
411
+ }
412
+ }
413
+ return errors
414
+ }),
415
+ label: computed(() => props.label), // Aktueller Label-Modus (gespiegelt von props.label)
416
+ // Ob der Bone-Tooltip angezeigt wird (bei hidden/placeholder-Label oder showBoneTooltip=true)
417
+ showtooltip: computed(() => {
418
+ if (props.showBoneTooltip) {
419
+ return true
420
+ }
421
+ if (["hide", "placeholder"].includes(props.label)) {
422
+ return true
423
+ }
424
+ return false
425
+ }),
426
+ defaultLanguage: computed(() => props.defaultLanguage), // Standardsprache (gespiegelt von props.defaultLanguage)
427
+ currentLanguage: null, // Aktuell aktiver Sprach-Tab
428
+ debug: computed(() => props.debug), // Debug-Modus (gespiegelt von props.debug)
429
+ labelTooltipTargetId: computed(() => buildBoneDomId(unref(bonePath) ?? [], "label-tooltip")), // Eindeutige DOM-ID für den Label-Tooltip-Anker
430
+ boneTooltipTargetId: computed(() => buildBoneDomId(unref(bonePath) ?? [], "field-tooltip")), // Eindeutige DOM-ID für den Bone-Tooltip-Anker
431
+ // Ob für diesen Bone überhaupt Actions auflösbar sind (für UI-Wrapper-Sichtbarkeit).
432
+ // MIT demselben Extra-Kontext und derselben Konfigurations-Merge-Reihenfolge
433
+ // wie BoneActions selbst — sonst fällt z.B. die Relations-Auswahl
434
+ // (when: relationSelector) hier raus und der Wrapper versteckt eine real
435
+ // vorhandene Action, oder eine per ViForm abgewählte bleibt "sichtbar".
436
+ resolvedActions: computed(() =>
437
+ resolveActions(
438
+ state.bonestructure,
439
+ [...formConfiguredActions(boneActionsConfig, props.name), ...(state.bonestructure?.params?.actions ?? [])],
440
+ state,
441
+ { relationSelector }
442
+ )
443
+ ),
444
+ hasActions: computed(() => state.resolvedActions.length > 0),
445
+ // Pflicht-Actions (Löschen, Download …) erscheinen auch ohne `boneactions`
446
+ hasRequiredActions: computed(() => state.resolvedActions.some((action) => action.required)),
447
+ hasOptionalActions: computed(() => state.resolvedActions.some((action) => !action.required)),
448
+ })
449
+ provide("boneState", state)
450
+
451
+ /**
452
+ * Weist jedem Objekt im Array ein stabiles _key-Attribut (UUID) zu, falls noch keines vorhanden ist.
453
+ * Wird via watchEffect aufgerufen um stabile :key-Bindings für vue-draggable sicherzustellen.
454
+ * @param {Array<Object>} arr - Multiple-Array dessen Einträge Objekte sind
455
+ */
456
+ function ensureKeys(arr) {
457
+ for (const it of arr) {
458
+ if (!it._key && typeof it === "object") {
459
+ it._key = crypto.randomUUID()
460
+ }
461
+ }
462
+ }
463
+ watchEffect(() => {
464
+ if (Array.isArray(state.bonevalue)) {
465
+ ensureKeys(state.bonevalue)
466
+ }
467
+ })
468
+
469
+ /**
470
+ * Aktualisiert state.bonevalue anhand von lang/index-Kontext und feuert change-internal.
471
+ * Bei valid=false wird der Wert nicht gespeichert, das Event aber trotzdem gefeuert.
472
+ * Bei state.readonly wird kein Event gefeuert.
473
+ * @param {string} name - Bone-Name
474
+ * @param {unknown} val - Neuer Wert
475
+ * @param {string|null} lang - Sprach-Key (bei Multilang-Bones)
476
+ * @param {number|null} index - Index (bei Multiple-Bones)
477
+ * @param {boolean} valid - Ob der Wert valide ist
478
+ * @returns {false|void}
479
+ */
480
+ function updateValue(name, val, lang = null, index = null, valid = true) {
481
+ if (val === undefined) return false
482
+ if (valid) {
483
+ if (lang) {
484
+ if (!state.bonevalue) {
485
+ state.bonevalue = {}
486
+ }
487
+ if (Object.keys(state.bonevalue).includes(lang) && index !== null) {
488
+ state.bonevalue[lang][index] = val
489
+ } else {
490
+ state.bonevalue[lang] = val
491
+ }
492
+ } else if (index !== null) {
493
+ state.bonevalue[index] = val
494
+ } else {
495
+ state.bonevalue = val
496
+ }
497
+ }
498
+
499
+ if (state.readonly) return false
500
+
501
+ let changeInternalObj = {
502
+ name: name,
503
+ value: val,
504
+ lang: lang,
505
+ index: index,
506
+ valid: valid,
507
+ }
508
+
509
+ emit("change-internal", changeInternalObj)
510
+ }
511
+
512
+ /**
513
+ * Fügt einen neuen Eintrag zum Multiple-Array hinzu.
514
+ * Wird via provide("addMultipleEntry") an Actionbars weitergegeben.
515
+ * @param {string|null} lang - Sprach-Key (bei Multilang-Bones)
516
+ * @param {unknown} data - Initialer Wert des neuen Eintrags
517
+ */
518
+ function addMultipleEntry(lang = null, data = "") {
519
+ if (lang) {
520
+ if (Object.keys(state.bonevalue).includes(lang)) {
521
+ state.bonevalue[lang].push(data)
522
+ } else {
523
+ state.bonevalue[lang] = [data]
524
+ }
525
+ } else {
526
+ if (state.bonevalue) {
527
+ state.bonevalue.push(data)
528
+ } else {
529
+ state.bonevalue = [data]
530
+ }
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Feuert updateValue nach einer Multiple-Array-Mutation (splice).
536
+ * @param {string|null} lang - Sprach-Key oder null
537
+ */
538
+ function emitMultipleUpdate(lang) {
539
+ if (lang) {
540
+ updateValue(props.name, state.bonevalue[lang], lang)
541
+ } else {
542
+ updateValue(props.name, state.bonevalue)
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Entfernt einen einzelnen Eintrag aus dem Multiple-Array per Index.
548
+ * @param {number} index - Index des zu entfernenden Eintrags
549
+ * @param {string|null} lang - Sprach-Key (bei Multilang-Bones)
550
+ */
551
+ function removeMultipleEntry(index, lang = null) {
552
+ if (lang) {
553
+ state.bonevalue?.[lang].splice(index, 1)
554
+ } else {
555
+ state.bonevalue.splice(index, 1)
556
+ }
557
+ emitMultipleUpdate(lang)
558
+ }
559
+
560
+ /**
561
+ * Entfernt alle Einträge aus dem Multiple-Array.
562
+ * Wird via provide("removeMultipleEntries") an Actionbars weitergegeben.
563
+ * @param {string|null} lang - Sprach-Key (bei Multilang-Bones)
564
+ */
565
+ function removeMultipleEntries(lang = null) {
566
+ if (lang) {
567
+ state.bonevalue?.[lang].splice(0)
568
+ } else {
569
+ state.bonevalue.splice(0)
570
+ }
571
+ emitMultipleUpdate(lang)
572
+ }
573
+
574
+ /**
575
+ * Wechselt die aktive Sprache anhand des data-lang-Attributs des geklickten Tab-Elements.
576
+ * @param {Event} e - Click-Event vom wa-tab
577
+ */
578
+ function updateLanguage(e) {
579
+ state.currentLanguage = e.target.dataset.lang
580
+ }
581
+
582
+ /**
583
+ * Delegiert an Utils.formatString — wird via provide("formatString") für Relational-Bones bereitgestellt.
584
+ * @param {string} formatstr - Format-Template
585
+ * @param {unknown} boneValue - Bone-Wert der in das Template eingesetzt wird
586
+ * @returns {string}
587
+ */
588
+ function formatString(formatstr, boneValue) {
589
+ return Utils.formatString(formatstr, boneValue)
590
+ }
591
+
592
+ /**
593
+ * Übersetzt einen Fehler-Key via vue-i18n (errors.{key}). Fällt auf den rohen String zurück.
594
+ * @param {string} error - Fehler-Key oder roher Fehlertext
595
+ * @returns {string}
596
+ */
597
+ function errorMessage(error) {
598
+ const key = `errors.${error}`
599
+ let temp = t(key)
600
+ if (temp === key) {
601
+ temp = t(error)
602
+ }
603
+ return temp
604
+ }
605
+
606
+ watch(
607
+ () => props.skel?.[props.name],
608
+ () => {
609
+ state.bonevalue = props.skel?.[props.name]
610
+ }
611
+ )
612
+
613
+ // Sprach-Tab wählen: die gewünschte Sprache — führt der Bone sie nicht,
614
+ // die erste seiner Sprachen (sonst wäre KEIN Tab aktiv).
615
+ function pickLanguage(wanted) {
616
+ if (!state.languages?.length || state.languages.includes(wanted)) return wanted
617
+ return state.languages[0]
618
+ }
619
+
620
+ watch(
621
+ () => state.defaultLanguage,
622
+ (newVal) => {
623
+ state.currentLanguage = pickLanguage(newVal)
624
+ }
625
+ )
626
+
627
+ onBeforeMount(() => {
628
+ if (props.value) {
629
+ state.bonevalue = props.value
630
+ } else {
631
+ state.bonevalue = props.skel?.[props.name]
632
+ }
633
+ state.currentLanguage = pickLanguage(state.defaultLanguage)
634
+ })
635
+
636
+ provide("updateValue", updateValue)
637
+ provide("addMultipleEntry", addMultipleEntry)
638
+ // Für die Pflicht-Action "removeEntry" (Zeilen-X der Multiple-Listen).
639
+ provide("removeMultipleEntry", removeMultipleEntry)
640
+ provide("removeMultipleEntries", removeMultipleEntries)
641
+ provide("formatString", formatString)
642
+ </script>
643
+
644
+ <style scoped>
645
+ @layer foundation.form {
646
+ /* Bone-Wrapper: Label links, Inhalt rechts (auf Mobile gestapelt) */
647
+ .bone {
648
+ display: grid;
649
+ grid-template-columns: 235px 1fr;
650
+ grid-gap: var(--wa-space-s);
651
+ margin-bottom: 20px;
652
+
653
+ /* Label über dem Inhalt oder gar keins: eine Spalte auf jeder Breite */
654
+ &.is-label-top,
655
+ &.is-label-hide {
656
+ grid-template-columns: 1fr;
657
+ }
658
+
659
+ @media (max-width: 900px) {
660
+ grid-template-columns: 1fr;
661
+ }
662
+ }
663
+ .bone-label-required {
664
+ color: var(--wa-color-brand-50);
665
+ font-weight: 700;
666
+ }
667
+
668
+ /* Fehler-Callout unter dem Bone-Inhalt */
669
+ wa-callout.bone-error {
670
+ padding: var(--wa-space-xs);
671
+ border: 1px solid var(--wa-color-border-loud);
672
+ border-radius: var(--wa-border-radius-m);
673
+ color: var(--wa-color-on-quiet);
674
+
675
+ & wa-icon {
676
+ margin-inline-end: var(--wa-space-2xs);
677
+ }
678
+ }
679
+
680
+ /* Sprachumschaltung: Tabs unter dem Eingabefeld (Multilang-Bones) */
681
+ .bone-lang-tabs {
682
+ width: 100%;
683
+ --track-width: 0;
684
+
685
+ &::part(body) {
686
+ overflow-x: hidden;
687
+ }
688
+
689
+ & wa-tab-panel::part(base) {
690
+ padding: 0;
691
+ }
692
+
693
+ & wa-tab {
694
+ &::part(base) {
695
+ padding: var(--wa-space-xs);
696
+ background-color: var(--wa-color-neutral-80);
697
+ border-radius: 0;
698
+ border-left: 1px solid var(--wa-color-border-loud);
699
+ border-bottom: 1px solid var(--wa-color-border-loud);
700
+ }
701
+
702
+ &:hover::part(base) {
703
+ background-color: var(--wa-color-neutral-90);
704
+ }
705
+
706
+ &[aria-selected="true"] {
707
+ &::part(base) {
708
+ border-bottom: 2px solid var(--wa-color-brand-50);
709
+ background-color: var(--wa-color-fill-quiet);
710
+ }
711
+ }
712
+
713
+ /* abgerundete Außenecken; Nachbar-Tabs überlappen sich um 1px */
714
+ &:first-child::part(base) {
715
+ margin-left: 1rem;
716
+ border-bottom-left-radius: 5px;
717
+ }
718
+
719
+ &:nth-last-child(2)::part(base) {
720
+ border-bottom-right-radius: 5px;
721
+ border-right: 1px solid var(--wa-color-border-loud);
722
+ }
723
+
724
+ &:not(:first-child)::part(base) {
725
+ margin-left: -1px;
726
+ }
727
+ }
728
+ }
729
+ }
730
+ </style>
731
+ <style>
732
+ * {
733
+ box-sizing: border-box;
734
+ }
735
+ </style>
736
+ <!--
737
+ <style scoped>
738
+ @layer foundation.form {
739
+ .bone-wrapper {
740
+ display: grid;
741
+ grid-template-columns: 235px 1fr;
742
+ grid-gap: var(--wa-space-s);
743
+ margin-bottom: 20px;
744
+
745
+ &.bone-wrapper-record,
746
+ &.label-top,
747
+ &.has-subbones {
748
+ display: flex;
749
+ flex-direction: column;
750
+ grid-gap: 0;
751
+
752
+ & > :deep(.bone-name) {
753
+ border-bottom-left-radius: 0;
754
+ border-top-right-radius: var(--wa-border-radius-m);
755
+ min-width: 235px;
756
+ }
757
+
758
+ & > .bone-inner-wrap {
759
+ padding-top: var(--wa-space-s);
760
+ border-top: 2px solid var(--wa-color-neutral-80);
761
+ margin-bottom: 5px;
762
+ }
763
+
764
+ & .multiple-bone {
765
+ border-bottom: 1px solid var(--wa-color-neutral-80);
766
+ padding-bottom: var(--wa-space-s);
767
+ margin-bottom: var(--wa-space-s);
768
+
769
+ &:deep(wa-details) {
770
+ &:last-child {
771
+ &::part(base) {
772
+ border-bottom: none;
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }
778
+
779
+ &.label-hide {
780
+ display: flex;
781
+ flex-direction: column;
782
+ grid-gap: 0;
783
+ }
784
+
785
+ @media (max-width: 900px) {
786
+ grid-template-columns: 1fr;
787
+ }
788
+ }
789
+
790
+ wa-tab-panel::part(base) {
791
+ padding: 0;
792
+ }
793
+
794
+ .lang-tab {
795
+ width: 100%;
796
+ --track-width: 0;
797
+ --indicator-color: var(--vi-background-color);
798
+ --track-color: var(--vi-border-color);
799
+
800
+ &::part(body) {
801
+ padding-bottom: var(--wa-space-xs);
802
+ overflow-x: hidden;
803
+ }
804
+
805
+ &::part(tabs) {
806
+ border-top: 1px solid var(--vi-border-color);
807
+ }
808
+
809
+ & wa-tab {
810
+ margin-top: -1px;
811
+
812
+ &[aria-selected="true"] {
813
+ z-index: 1;
814
+
815
+ &::part(base) {
816
+ background-color: var(--vi-background-color);
817
+ border: 1px solid var(--vi-border-color);
818
+ border-top: 1px solid var(--vi-background-color) !important;
819
+ border-bottom: 2px solid var(--wa-color-brand-50) !important;
820
+ }
821
+ }
822
+
823
+ &::part(base) {
824
+ background-color: var(--wa-color-neutral-90);
825
+ border: 1px solid var(--wa-color-neutral-70);
826
+ border-top: 1px solid var(--vi-border-color);
827
+ border-radius: 0;
828
+ padding: var(--wa-space-xs);
829
+ transition: all ease 0.3s;
830
+ }
831
+
832
+ &:hover {
833
+ &::part(base) {
834
+ background-color: var(--vi-background-color);
835
+ border: 1px solid var(--vi-border-color);
836
+ }
837
+ }
838
+
839
+ &:first-child {
840
+ &::part(base) {
841
+ border-bottom-left-radius: 5px;
842
+ }
843
+ }
844
+
845
+ &:nth-last-child(2) {
846
+ &::part(base) {
847
+ border-bottom-right-radius: 5px;
848
+ }
849
+ }
850
+
851
+ &:not(:first-child) {
852
+ &::part(base) {
853
+ margin-left: -1px;
854
+ }
855
+ }
856
+ }
857
+ }
858
+
859
+ .multiple-placeholder {
860
+ &:not(.readonly) {
861
+ margin-bottom: var(--wa-space-xs);
862
+ }
863
+
864
+ & wa-input {
865
+ &::part(base) {
866
+ border-top-left-radius: 0;
867
+ border-bottom-left-radius: 0;
868
+ opacity: 0.7;
869
+ }
870
+ }
871
+
872
+ @media (max-width: 900px) {
873
+ & wa-input {
874
+ &::part(base) {
875
+ border-top-right-radius: 0;
876
+ border-bottom-left-radius: var(--wa-border-radius-m);
877
+ }
878
+ }
879
+ }
880
+ }
881
+
882
+ .multiple-bone {
883
+ margin-bottom: var(--wa-space-xs);
884
+
885
+ &:first-child {
886
+ padding-top: var(--wa-space-xs);
887
+ }
888
+
889
+ & .bone-wrapper {
890
+ margin-bottom: var(--wa-space-xs);
891
+ }
892
+
893
+ &:first-child {
894
+ & :deep(.value-line) {
895
+ }
896
+ }
897
+ }
898
+
899
+ .bone-inner-wrap {
900
+ min-width: 1px;
901
+
902
+ & wa-alert {
903
+ margin-top: var(--wa-space-xs);
904
+ background-color: transparent;
905
+
906
+ &::part(message) {
907
+ padding: var(--wa-space-xs) var(--wa-space-s);
908
+ }
909
+
910
+ &::part(icon) {
911
+ padding-left: var(--wa-space-s);
912
+ }
913
+ }
914
+ }
915
+
916
+ .label-info {
917
+ &::part(base) {
918
+ align-items: center;
919
+ margin-left: 0.5em;
920
+ background-color: var(--wa-color-info-95);
921
+ border: none;
922
+ overflow: visible;
923
+ }
924
+
925
+ &::part(message) {
926
+ padding: var(--wa-space-s);
927
+ }
928
+
929
+ &::part(icon) {
930
+ display: flex;
931
+ align-items: center;
932
+ justify-content: center;
933
+ border-radius: 50%;
934
+ background-color: var(--wa-color-surface-default);
935
+ width: 1em;
936
+ height: 1em;
937
+ margin-left: -0.5em;
938
+ padding-left: 0;
939
+ }
940
+ }
941
+
942
+ .tooltip {
943
+ display: flex;
944
+ align-items: center;
945
+ justify-content: center;
946
+ margin-left: auto;
947
+ padding-left: 0.4em;
948
+
949
+ & wa-icon {
950
+ background-color: var(--wa-color-info-50);
951
+ color: #fff;
952
+ padding: 0.4em;
953
+ border-radius: 50%;
954
+ font-size: 0.55em;
955
+ }
956
+ }
957
+
958
+ wa-tooltip {
959
+ &::part(body) {
960
+ background-color: var(--wa-color-info-50);
961
+ }
962
+
963
+ &::part(base__arrow) {
964
+ background-color: var(--wa-color-info-50);
965
+ }
966
+ }
967
+
968
+ .required {
969
+ color: var(--wa-color-brand-50);
970
+ font-weight: 700;
971
+ }
972
+
973
+ :deep(wa-combobox) {
974
+ &::part(input__base) {
975
+ border: 1px solid var(--vi-border-color);
976
+ box-shadow: none !important;
977
+ }
978
+
979
+ &::part(input__prefix) {
980
+ display: none !important;
981
+ }
982
+
983
+ &::part(input__suffix) {
984
+ display: none !important;
985
+ }
986
+ }
987
+
988
+ wa-tooltip.bone-tooltip {
989
+ &::part(body) {
990
+ background-color: var(--wa-color-brand-50);
991
+ }
992
+
993
+ &::part(base__arrow) {
994
+ background-color: var(--wa-color-brand-50);
995
+ }
996
+ }
997
+ }
998
+
999
+ .error-msg::part(base) {
1000
+ background-color: #ffecec;
1001
+ color: var(--wa-color-danger-30);
1002
+ }
1003
+
1004
+ .wrapper-bone-row {
1005
+ display: flex;
1006
+ flex-direction: row;
1007
+ gap: var(--wa-space-xs);
1008
+ }
1009
+
1010
+ .wrapper-bone-block {
1011
+ display: block;
1012
+ width: 100%;
1013
+ }
1014
+ </style>
1015
+
1016
+ <style>
1017
+ wa-input[data-user-invalid]::part(base),
1018
+ wa-select[data-user-invalid]::part(combobox),
1019
+ wa-checkbox[data-user-invalid]::part(control) {
1020
+ border-color: var(--wa-color-danger-50);
1021
+ }
1022
+ </style>
1023
+ -->