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,225 @@
1
+ import rawBone from "./widgets/rawBone.vue"
2
+ import keyBone from "./widgets/keyBone.vue"
3
+ import stringBone from "./widgets/stringBone.vue"
4
+ import emailBone from "./widgets/emailBone.vue"
5
+ import dateBone from "./widgets/dateBone.vue"
6
+ import selectBone from "./widgets/selectBone.vue"
7
+ import booleanBone from "./widgets/booleanBone.vue"
8
+ import passwordBone from "./widgets/passwordBone.vue"
9
+ import recordBone from "./widgets/recordBone.vue"
10
+ import colorBone from "./widgets/colorBone.vue"
11
+ import numericBone from "./widgets/numericBone.vue"
12
+ import relationalBone from "./widgets/relationalBone.vue"
13
+ import fileBone from "./widgets/fileBone.vue"
14
+ import spatialBone from "./widgets/spatialBone.vue"
15
+
16
+ import booleanBoneSelect from "./widgets/booleanBoneSelect.vue"
17
+ import booleanBoneChoose from "./widgets/booleanBoneChoose.vue"
18
+ import selectBoneChoose from "./widgets/selectBoneChoose.vue"
19
+ import relationalBoneSelect from "./widgets/relationalBoneSelect.vue"
20
+
21
+ import defaultBar from "./actionbar/defaultBar.vue"
22
+ import relationalBar from "./actionbar/relationalBar.vue"
23
+ import fileBar from "./actionbar/fileBar.vue"
24
+
25
+ import { reactive, shallowRef, defineAsyncComponent } from "vue"
26
+ import { defineStore } from "pinia"
27
+
28
+ // Heavy bones — lazy loaded so consumers don't pay for their deps unless used.
29
+ // json-editor-vue (~60KB+), libphonenumber-js (~150KB), CodeMirror suite (~200KB+),
30
+ // CKEditor5 suite (~400KB+) only land in the bundle when a form actually renders
31
+ // the corresponding bone type.
32
+ const jsonBone = defineAsyncComponent(() => import("./widgets/jsonBone.vue"))
33
+ const phoneBone = defineAsyncComponent(() => import("./widgets/phoneBone.vue"))
34
+ const codeBone = defineAsyncComponent(() => import("./widgets/codeBone.vue"))
35
+ const textBone = defineAsyncComponent(() => import("./widgets/textBone.vue"))
36
+
37
+ export const useBoneStore = defineStore("boneStore", () => {
38
+ const state = reactive({
39
+ additionalBones: shallowRef({}),
40
+ defaultBones: shallowRef({
41
+ rawBone,
42
+ keyBone,
43
+ stringBone,
44
+ emailBone,
45
+ dateBone,
46
+ booleanBone,
47
+ selectBone,
48
+ passwordBone,
49
+ recordBone,
50
+ numericBone,
51
+ colorBone,
52
+ relationalBone,
53
+ jsonBone,
54
+ fileBone,
55
+ textBone,
56
+ spatialBone,
57
+ phoneBone,
58
+ booleanBoneSelect,
59
+ booleanBoneChoose,
60
+ selectBoneChoose,
61
+ relationalBoneSelect,
62
+ codeBone,
63
+ }),
64
+ actionbars: shallowRef({
65
+ "relational.tree.leaf.file.file": fileBar,
66
+ "relational.": relationalBar,
67
+ }),
68
+ multibones: shallowRef(["select", "select."]),
69
+ })
70
+
71
+ function addBoneWidget(boneType, widget) {
72
+ state.additionalBones[boneType] = widget
73
+ }
74
+
75
+ function importWidgets() {
76
+ let bones = state.defaultBones
77
+ for (const [k, v] of Object.entries(state.additionalBones)) {
78
+ bones.add(v)
79
+ }
80
+ return bones
81
+ }
82
+
83
+ function getBoneWidget(boneType) {
84
+ if (Object.keys(state.additionalBones).includes(boneType)) {
85
+ //exact match
86
+ return state.additionalBones[boneType]
87
+ } else {
88
+ let typeParts = boneType.split(".") //prefix match
89
+ let matchingPrefixes = Object.entries(state.additionalBones).filter((prefix) =>
90
+ prefix[0].startsWith(typeParts[0] + ".")
91
+ )
92
+ if (matchingPrefixes.length > 0) {
93
+ matchingPrefixes.sort((a, b) => b.length - a.length)
94
+ for (let prefix of matchingPrefixes) {
95
+ if (boneType.startsWith(prefix[0])) {
96
+ return state.additionalBones[prefix[0]]
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ if (boneType === "date" || boneType.startsWith("date.")) {
103
+ return dateBone
104
+ } else if (boneType === "key") {
105
+ return keyBone
106
+ } else if (boneType === "str.email") {
107
+ return emailBone
108
+ } else if (boneType === "str.phone") {
109
+ return phoneBone
110
+ } else if (boneType === "str" || boneType.startsWith("str.")) {
111
+ return stringBone
112
+ } else if (boneType === "select.choose") {
113
+ return selectBoneChoose
114
+ } else if (boneType === "select" || boneType.startsWith("select.")) {
115
+ return selectBone
116
+ } else if (boneType === "bool.choose") {
117
+ return booleanBoneChoose
118
+ } else if (boneType === "bool.select") {
119
+ return booleanBoneSelect
120
+ } else if (boneType === "bool" || boneType.startsWith("bool.")) {
121
+ return booleanBone
122
+ } else if (boneType === "password" || boneType.startsWith("password.")) {
123
+ return passwordBone
124
+ } else if (boneType === "record" || boneType.startsWith("record.")) {
125
+ return recordBone
126
+ } else if (boneType === "numeric" || boneType.startsWith("numeric.")) {
127
+ return numericBone
128
+ } else if (boneType === "relational.tree.leaf.file.file" || boneType.startsWith("relational.tree.leaf.file.")) {
129
+ return fileBone
130
+ } else if (boneType === "relational.select" || boneType.startsWith("relational.select.")) {
131
+ return relationalBoneSelect
132
+ } else if (boneType === "relational" || boneType.startsWith("relational.")) {
133
+ return relationalBone
134
+ } else if (boneType === "raw.json") {
135
+ return jsonBone
136
+ } else if (boneType.startsWith("raw.code.")) {
137
+ return codeBone
138
+ } else if (boneType === "color" || boneType.startsWith("color.")) {
139
+ return colorBone
140
+ } else if (boneType === "text" || boneType.startsWith("text.")) {
141
+ return textBone
142
+ } else if (boneType === "spatial" || boneType.startsWith("spatial.")) {
143
+ return spatialBone
144
+ }
145
+
146
+ return rawBone
147
+ }
148
+
149
+ function addBoneActionbar(boneType, widget) {
150
+ state.actionbars[boneType] = widget
151
+ }
152
+
153
+ function getBoneActionbar(boneType) {
154
+ if (Object.keys(state.actionbars).includes(boneType)) {
155
+ //exact match
156
+ return state.actionbars[boneType]
157
+ } else {
158
+ let typeParts = boneType.split(".") //prefix match
159
+ let matchingPrefixes = Object.entries(state.actionbars).filter((prefix) =>
160
+ prefix[0].startsWith(typeParts[0] + ".")
161
+ )
162
+ if (matchingPrefixes.length > 0) {
163
+ matchingPrefixes.sort((a, b) => b.length - a.length)
164
+ for (let prefix of matchingPrefixes) {
165
+ if (boneType.startsWith(prefix[0])) {
166
+ return state.actionbars[prefix[0]]
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ return defaultBar
173
+ }
174
+
175
+ return {
176
+ state,
177
+ addBoneWidget,
178
+ getBoneWidget,
179
+ importWidgets,
180
+
181
+ addBoneActionbar,
182
+ getBoneActionbar,
183
+ }
184
+ })
185
+
186
+ export function addBoneActionbar(boneType, widget) {
187
+ const boneStore = useBoneStore()
188
+ return boneStore.addBoneActionbar(boneType, widget)
189
+ }
190
+
191
+ export function getBoneActionbar(boneType) {
192
+ const boneStore = useBoneStore()
193
+ return boneStore.getBoneActionbar(boneType)
194
+ }
195
+
196
+ export function getBoneWidget(boneType) {
197
+ const boneStore = useBoneStore()
198
+ return boneStore.getBoneWidget(boneType)
199
+ }
200
+
201
+ export function addBoneWidget(boneType, widget) {
202
+ const boneStore = useBoneStore()
203
+ boneStore.addBoneWidget(boneType, widget)
204
+ }
205
+
206
+ export function BoneHasMultipleHandling(boneType) {
207
+ const boneStore = useBoneStore()
208
+ if (boneStore.state.multibones.includes(boneType)) {
209
+ return true
210
+ } else {
211
+ let typeParts = boneType.split(".") //prefix match
212
+ let matchingPrefixes = Object.entries(boneStore.state.multibones).filter((prefix) =>
213
+ prefix[1].startsWith(typeParts[0] + ".")
214
+ )
215
+ if (matchingPrefixes.length > 0) {
216
+ matchingPrefixes.sort((a, b) => b.length - a.length)
217
+ for (let prefix of matchingPrefixes) {
218
+ if (boneType.startsWith(prefix[1])) {
219
+ return true
220
+ }
221
+ }
222
+ }
223
+ }
224
+ return false
225
+ }
@@ -0,0 +1,46 @@
1
+ import { downloadUrlFor, uploadFile } from "../../request/index.js"
2
+
3
+ class ViURUploadAdapter {
4
+ constructor(loader, api_url) {
5
+ this.loader = loader
6
+ this.api_url = api_url
7
+ }
8
+
9
+ async upload() {
10
+ this.loader.uploadTotal = 100
11
+ this.loader.uploaded = 0
12
+ return this.loader.file.then(
13
+ async (file) =>
14
+ new Promise((resolve, reject) => {
15
+ this.loader.uploaded = 10
16
+ uploadFile(file, { renderer: "vi" })
17
+ .then((uploadedData) => {
18
+ this.loader.uploaded = 75
19
+ this.loader.uploaded = 100
20
+ resolve({
21
+ // Die URL landet im HTML des Text-Bones: als /file/download/…
22
+ // (nur absolut gemacht) — die erkennt viur-core wieder
23
+ // (Blob-Referenz, srcset) und sie liegt nicht hinter /vi.
24
+ default: downloadUrlFor(uploadedData["downloadUrl"]),
25
+ })
26
+ })
27
+ .catch((error) => {
28
+ reject(error)
29
+ })
30
+ })
31
+ )
32
+ }
33
+
34
+ // Aborts the upload process.
35
+ abort() {
36
+ if (this.xhr) {
37
+ this.xhr.abort()
38
+ }
39
+ }
40
+ }
41
+
42
+ export function ViURUploadAdapterPlugin(editor) {
43
+ editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
44
+ return new ViURUploadAdapter(loader, editor.config.get("viur_api_url"))
45
+ }
46
+ }
@@ -0,0 +1,284 @@
1
+ export default class Utils {
2
+ static objectEmpty(obj) {
3
+ return Object.keys(obj).length === 0 && obj.constructor === Object
4
+ }
5
+
6
+ static getDescr(bone, value) {
7
+ try {
8
+ return bone["values"].filter((option) => option[0] === value)[0][1]
9
+ } catch (e) {
10
+ return "-"
11
+ }
12
+ }
13
+
14
+ static stripHtml(html) {
15
+ const div = document.createElement("div")
16
+ div.innerHTML = html
17
+ return div.textContent || div.innerText || ""
18
+ }
19
+
20
+ static unescape(value) {
21
+ if (!value) value = ""
22
+ return String(value)
23
+ .replace(/&lt;/g, "<")
24
+ .replace(/&gt;/g, ">")
25
+ .replace(/&quot;/g, '"')
26
+ .replace(/&#39;/g, "'")
27
+ .replace(/&#40;/g, "(")
28
+ .replace(/&#41;/g, ")")
29
+ .replace(/&#61;/g, "=")
30
+ .replace(/&#039;/g, "'")
31
+ .replace(/&#040;/g, "(")
32
+ .replace(/&#041;/g, ")")
33
+ .replace(/&#061;/g, "=")
34
+ }
35
+
36
+ static formatString(formatstr, boneValue, fallback = "-") {
37
+ function getpathListFromFormatstring(formatstr) {
38
+ let output = []
39
+ let formatList = []
40
+ let regstr = /\$\((.*?)\)/g
41
+
42
+ while (formatList) {
43
+ formatList = regstr.exec(formatstr)
44
+ if (!formatList) {
45
+ formatList = false
46
+ continue
47
+ }
48
+
49
+ output.push(formatList[1])
50
+ }
51
+
52
+ return output
53
+ }
54
+
55
+ function readValue(pathstr, avalue) {
56
+ let path = pathstr.split(".")
57
+ let restPath = pathstr.split(".")
58
+ let aval = avalue
59
+ for (let entry of path) {
60
+ restPath.shift()
61
+ if (aval && aval !== "-" && Object.keys(aval).includes(entry)) {
62
+ if (Array.isArray(aval[entry])) {
63
+ let resVal = []
64
+ for (let val of aval[entry]) {
65
+ resVal.push(readValue(restPath.join("."), val))
66
+ }
67
+ aval = resVal.join(", ")
68
+ } else {
69
+ aval = readValue(restPath.join("."), aval[entry])
70
+ }
71
+
72
+ if (typeof aval === "object" && !Array.isArray(aval) && aval !== null) {
73
+ aval = Object.entries(aval)
74
+ .map((x) => x[1])
75
+ .join(", ")
76
+ }
77
+ } else if (!aval || (typeof aval[entry] === "object" && !aval[entry])) {
78
+ aval = "-"
79
+ }
80
+ }
81
+ return aval
82
+ }
83
+
84
+ let pathlist = getpathListFromFormatstring(formatstr)
85
+
86
+ let finalStrList = []
87
+
88
+ if (!Array.isArray(boneValue)) {
89
+ boneValue = [boneValue]
90
+ }
91
+
92
+ for (let avalue of boneValue) {
93
+ let finalstr = formatstr
94
+ for (let pathstr of pathlist) {
95
+ let aval = readValue(pathstr, avalue)
96
+ // Auch Zahlen und Wahrheitswerte gehören in den Text: vorher wurde
97
+ // JEDER Nicht-String durch den Fallback ersetzt, `$(dest.kdnr)` mit 42
98
+ // verschwand also. Leeres, Objekte und der Platzhalter "-" bleiben der
99
+ // Fallback.
100
+ if (aval === null || aval === undefined || aval === "-" || typeof aval === "object") {
101
+ aval = fallback
102
+ } else {
103
+ aval = String(aval)
104
+ }
105
+ aval = Utils.unescape(aval)
106
+ finalstr = finalstr.replace("$(" + pathstr + ")", aval)
107
+ }
108
+ finalStrList.push(finalstr)
109
+ }
110
+ return finalStrList.join(", ")
111
+ }
112
+
113
+ // ── ViUR-Werte als Text ───────────────────────────────────────────────
114
+ //
115
+ // EINE Stelle, die jeden Bone-Wert in eine Zeichenkette übersetzt — für
116
+ // Tabellen, Detailansichten, Export. Denn ein Bone-Wert ist selten ein
117
+ // Skalar: mehrsprachige Bones sind ein Objekt je Sprache, `multiple` eine
118
+ // Liste, relationale Bones tragen `{dest, rel}` und ihren eigenen
119
+ // Formatstring, `text` enthält HTML.
120
+
121
+ /** Typfamilie eines Bones: "str.email" → "str". */
122
+ static boneFamily(type) {
123
+ return String(type ?? "").split(".")[0]
124
+ }
125
+
126
+ /** select-Wert → Beschriftung. Akzeptiert Objekt- UND Paar-Schreibweise. */
127
+ static optionLabel(bone, value) {
128
+ const values = bone?.["values"]
129
+ if (Array.isArray(values)) {
130
+ const hit = values.find((entry) => String(entry?.[0]) === String(value))
131
+ return hit ? String(hit[1]) : String(value)
132
+ }
133
+ if (values && typeof values === "object") {
134
+ const hit = values[String(value)]
135
+ return hit === undefined ? String(value) : String(hit)
136
+ }
137
+ return String(value)
138
+ }
139
+
140
+ /** Ein EINZELNER Wert (ohne Sprachen/multiple — das macht boneToString). */
141
+ static singleValueToString(value, bone = {}, options = {}) {
142
+ if (value === null || value === undefined || value === "") return ""
143
+ switch (Utils.boneFamily(bone["type"])) {
144
+ case "bool": {
145
+ const [yes, no] = options.boolTexts ?? ["Ja", "Nein"]
146
+ return value ? yes : no
147
+ }
148
+ case "select":
149
+ return Utils.optionLabel(bone, value)
150
+ case "numeric": {
151
+ const numeric = Number(value)
152
+ if (!Number.isFinite(numeric)) return String(value)
153
+ const digits = typeof bone["precision"] === "number" ? bone["precision"] : undefined
154
+ return new Intl.NumberFormat(options.locale, {
155
+ minimumFractionDigits: digits,
156
+ maximumFractionDigits: digits,
157
+ }).format(numeric)
158
+ }
159
+ case "date": {
160
+ const parsed = new Date(String(value))
161
+ if (Number.isNaN(parsed.getTime())) return String(value)
162
+ // ViUR setzt die Flags `date`/`time` (beide standardmäßig an).
163
+ const wantsDate = bone["date"] !== false
164
+ const wantsTime = bone["time"] !== false
165
+ const format = {}
166
+ if (wantsDate || !wantsTime) Object.assign(format, { year: "numeric", month: "2-digit", day: "2-digit" })
167
+ if (wantsTime) Object.assign(format, { hour: "2-digit", minute: "2-digit" })
168
+ return new Intl.DateTimeFormat(options.locale, format).format(parsed)
169
+ }
170
+ case "relational":
171
+ case "record": {
172
+ // Relationale Bones bringen ihren Formatstring mit ("$(dest.name)").
173
+ const formatstr = bone["format"] ?? bone["params"]?.["format"] ?? "$(dest.name)"
174
+ const text = Utils.formatString(formatstr, value, "")
175
+ if (text && text !== "-") return Utils.stripHtml(String(text))
176
+ // Ohne Format ist der Schlüssel besser als nichts.
177
+ return String(value?.["dest"]?.["key"] ?? value?.["key"] ?? "")
178
+ }
179
+ case "text":
180
+ // Textbones enthalten HTML — in einer Zelle stört das.
181
+ return Utils.stripHtml(String(value))
182
+ case "raw":
183
+ return typeof value === "object" ? JSON.stringify(value) : String(value)
184
+ default:
185
+ return typeof value === "object" ? JSON.stringify(value) : String(value)
186
+ }
187
+ }
188
+
189
+ /** Ein Wert OHNE Sprachdimension (multiple-Listen inklusive) als Text. */
190
+ static monolingualToString(value, bone = {}, options = {}) {
191
+ if (bone["multiple"]) {
192
+ const list = Array.isArray(value) ? value : value === null || value === undefined ? [] : [value]
193
+ return list.map((entry) => Utils.singleValueToString(entry, bone, options)).join(options.separator ?? ", ")
194
+ }
195
+ return Utils.singleValueToString(value, bone, options)
196
+ }
197
+
198
+ /**
199
+ * Flagge einer Sprache — pflegefrei über die CLDR-Daten des Browsers:
200
+ * Sprachcode → wahrscheinlichste Region (`Intl.Locale.maximize`, de→DE,
201
+ * en→US, ja→JP) → Regional-Indicator-Emoji. `null`, wenn keine Region
202
+ * bestimmbar ist.
203
+ */
204
+ static languageFlag(code) {
205
+ try {
206
+ const region = new Intl.Locale(String(code)).maximize().region
207
+ if (!region || !/^[A-Z]{2}$/.test(region)) return null
208
+ return String.fromCodePoint(...[...region].map((char) => 0x1f1a5 + char.charCodeAt(0)))
209
+ } catch {
210
+ return null
211
+ }
212
+ }
213
+
214
+ /** Präfix, das einen Wert seiner Sprache zuordnet: Flagge, sonst Kürzel. */
215
+ static languageTag(code) {
216
+ return Utils.languageFlag(code) ?? `${code}:`
217
+ }
218
+
219
+ /**
220
+ * Bone-Wert eines Datensatzes als Text.
221
+ *
222
+ * Mehrsprachige Bones (`bone.languages`) steuert `options.language`:
223
+ * - `"*"` — ALLE befüllten Sprachen, je mit Flaggen-Präfix (`🇩🇪 Wert`),
224
+ * verkettet mit `languageSeparator` (Standard `", "`).
225
+ * `options.primaryLanguage` steht dabei vorn, die übrigen folgen in der
226
+ * Reihenfolge des Bones.
227
+ * - Sprachcode — der Wert dieser Sprache. Ist er leer, springt die Anzeige
228
+ * auf die erste befüllte Sprache und kennzeichnet das mit deren Flagge —
229
+ * eine leere Zelle verschweigt sonst, dass der Eintrag in anderer Sprache
230
+ * sehr wohl Inhalt hat.
231
+ * - nicht gesetzt — die erste Sprache des Bones, ohne Präfix (stabiles
232
+ * Verhalten für Formatstrings und andere Bestandsnutzer).
233
+ *
234
+ * @param {*} value Wert aus dem Skel
235
+ * @param {object} bone Bone-Beschreibung aus der `structure`
236
+ * @param {{ language?: string, primaryLanguage?: string, locale?: string, boolTexts?: [string, string], separator?: string, languageSeparator?: string }} [options]
237
+ */
238
+ static boneToString(value, bone = {}, options = {}) {
239
+ const languages = bone["languages"]
240
+ if (Array.isArray(languages) && languages.length && value && typeof value === "object") {
241
+ const isEmpty = (entry) =>
242
+ entry === null || entry === undefined || entry === "" || (Array.isArray(entry) && entry.length === 0)
243
+ if (options.language === "*") {
244
+ const primary = options.primaryLanguage
245
+ const ordered =
246
+ primary && languages.includes(primary) ? [primary, ...languages.filter((lang) => lang !== primary)] : languages
247
+ return ordered
248
+ .filter((lang) => !isEmpty(value[lang]))
249
+ .map((lang) => `${Utils.languageTag(lang)} ${Utils.monolingualToString(value[lang], bone, options)}`)
250
+ .join(options.languageSeparator ?? ", ")
251
+ }
252
+ let lang = options.language && languages.includes(options.language) ? options.language : languages[0]
253
+ if (options.language && isEmpty(value[lang])) {
254
+ lang = languages.find((other) => !isEmpty(value[other])) ?? lang
255
+ }
256
+ const text = Utils.monolingualToString(value[lang], bone, options)
257
+ // Präfix nur, wenn eine BESTIMMTE Sprache gewünscht war und eine andere kam.
258
+ return options.language && options.language !== lang && !isEmpty(value[lang])
259
+ ? `${Utils.languageTag(lang)} ${text}`
260
+ : text
261
+ }
262
+ return Utils.monolingualToString(value, bone, options)
263
+ }
264
+ }
265
+
266
+ function normalizeBonePathSegment(segment) {
267
+ return String(segment)
268
+ .trim()
269
+ .replace(/[^a-zA-Z0-9_-]+/g, "-")
270
+ .replace(/^-+|-+$/g, "")
271
+ }
272
+
273
+ export function buildBoneDomId(pathSegments, suffix = null) {
274
+ const normalizedSegments = pathSegments
275
+ .filter((segment) => segment !== null && segment !== undefined && segment !== "")
276
+ .map(normalizeBonePathSegment)
277
+ .filter(Boolean)
278
+
279
+ if (suffix) {
280
+ normalizedSegments.push(normalizeBonePathSegment(suffix))
281
+ }
282
+
283
+ return ["vi-bone", ...normalizedSegments].join("--")
284
+ }
@@ -0,0 +1,35 @@
1
+ <template>
2
+ {{ getBoneValue(boneName, state.option) }}
3
+ </template>
4
+
5
+ <script setup>
6
+ import boneLogic from "./boneLogic"
7
+ import { computed, defineComponent, reactive } from "vue"
8
+ import { useI18n } from "vue-i18n"
9
+
10
+ const props = defineProps({
11
+ boneName: String,
12
+ skel: Object,
13
+ structure: Object,
14
+ })
15
+
16
+ const state = reactive({
17
+ option: computed(() => {
18
+ if (props.structure[props.boneName]["type"] === "date") {
19
+ if (props.structure[props.boneName]["date"] && !props.structure[props.boneName]["time"]) {
20
+ return "date"
21
+ }
22
+
23
+ if (props.structure[props.boneName]["time"] && !props.structure[props.boneName]["date"]) {
24
+ return "time"
25
+ }
26
+ }
27
+ return null
28
+ }),
29
+ })
30
+
31
+ const { t } = useI18n()
32
+ const { getBoneValue, bones_state } = boneLogic(props.skel, props.structure, false, null, t)
33
+ </script>
34
+
35
+ <style scoped></style>