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,400 @@
1
+ // Reine Spalten-/Zell-Logik: Auflösen der Definitionen, Vergleich, Filter,
2
+ // Facetten, Aggregation. Absichtlich frei von Vue — so ist der ganze
3
+ // Datenpfad ohne DOM testbar.
4
+
5
+ import type {
6
+ AggregationKind,
7
+ Align,
8
+ ColumnFacets,
9
+ DataTableColumn,
10
+ FilterEntry,
11
+ FilterType,
12
+ ResolvedColumn,
13
+ Row,
14
+ SortEntry,
15
+ SortMode,
16
+ } from "./types"
17
+
18
+ /** Wert über einen Punktpfad lesen ("user.name"). */
19
+ export function getPath(row: Row, path: string): unknown {
20
+ if (!path.includes(".")) return row[path]
21
+ let current: unknown = row
22
+ for (const part of path.split(".")) {
23
+ if (current === null || current === undefined) return undefined
24
+ current = (current as Row)[part]
25
+ }
26
+ return current
27
+ }
28
+
29
+ export function columnId(def: DataTableColumn): string {
30
+ const id = def.id ?? def.field
31
+ if (!id) throw new Error("[DataTable] Spalte braucht `id` oder `field`")
32
+ return id
33
+ }
34
+
35
+ function defaultText(value: unknown): string {
36
+ if (value === null || value === undefined) return ""
37
+ if (value instanceof Date) return value.toISOString().slice(0, 10)
38
+ if (Array.isArray(value)) return value.map((v) => defaultText(v)).join(", ")
39
+ if (typeof value === "object") return JSON.stringify(value)
40
+ return String(value)
41
+ }
42
+
43
+ export function resolveColumns<R extends Row>(defs: DataTableColumn<R>[]): ResolvedColumn<R>[] {
44
+ return defs.map((def) => {
45
+ const id = columnId(def as DataTableColumn)
46
+ const hasAccessor = Boolean(def.field || def.value)
47
+ const get = def.value
48
+ ? (row: R) => def.value!(row)
49
+ : def.field
50
+ ? (row: R) => getPath(row, def.field!)
51
+ : () => undefined
52
+ const align: Align = def.align ?? "start"
53
+ return {
54
+ id,
55
+ def,
56
+ field: def.field,
57
+ label: def.label ?? id,
58
+ align,
59
+ headerAlign: def.headerAlign ?? align,
60
+ sortable: def.sortable ?? hasAccessor,
61
+ searchable: def.searchable ?? hasAccessor,
62
+ filterable: def.filterable ?? false,
63
+ filterType: def.filterType ?? "text",
64
+ hideable: def.hideable ?? true,
65
+ get,
66
+ text: (row: R) => {
67
+ const value = get(row)
68
+ return def.formatter ? def.formatter(value, row) : defaultText(value)
69
+ },
70
+ }
71
+ })
72
+ }
73
+
74
+ export function findColumn<R extends Row>(columns: ResolvedColumn<R>[], id: string): ResolvedColumn<R> | undefined {
75
+ return columns.find((column) => column.id === id)
76
+ }
77
+
78
+ // ── Sortierung ────────────────────────────────────────────────────────
79
+
80
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" })
81
+ const collatorCase = new Intl.Collator(undefined, { numeric: true })
82
+
83
+ function toTime(value: unknown): number {
84
+ if (value instanceof Date) return value.getTime()
85
+ if (typeof value === "number") return value
86
+ const parsed = Date.parse(String(value))
87
+ return Number.isNaN(parsed) ? Number.NaN : parsed
88
+ }
89
+
90
+ export function isEmptyValue(value: unknown): boolean {
91
+ return value === null || value === undefined || value === ""
92
+ }
93
+
94
+ /**
95
+ * Zusammengesetzte Zellwerte aufs Sortier-Atom reduzieren: bei Arrays zählt
96
+ * der ERSTE Eintrag, bei einfachen Objekten der ERSTE Wert — rekursiv, bis ein
97
+ * Primitivwert (oder Date) übrig ist. Damit sortieren `multiple`-Bones,
98
+ * mehrsprachige Werte ({ de: "…" }) und relationale Bones ({ dest: { … } })
99
+ * nach ihrem jeweils ersten Wert statt nach "[object Object]".
100
+ */
101
+ export function firstValueOf(value: unknown, depth = 0): unknown {
102
+ if (depth > 10 || value === null || value === undefined) return value
103
+ if (Array.isArray(value)) return firstValueOf(value[0], depth + 1)
104
+ if (typeof value === "object" && !(value instanceof Date)) {
105
+ const values = Object.values(value as Record<string, unknown>)
106
+ return values.length ? firstValueOf(values[0], depth + 1) : undefined
107
+ }
108
+ return value
109
+ }
110
+
111
+ /** Vergleich zweier Zellwerte in einem der eingebauten Modi (aufsteigend). */
112
+ export function compareValues(a: unknown, b: unknown, mode: SortMode = "auto"): number {
113
+ switch (mode) {
114
+ case "basic":
115
+ return a === b ? 0 : (a as number) > (b as number) ? 1 : -1
116
+ case "number": {
117
+ const na = Number(a)
118
+ const nb = Number(b)
119
+ if (Number.isNaN(na) && Number.isNaN(nb)) return 0
120
+ if (Number.isNaN(na)) return 1
121
+ if (Number.isNaN(nb)) return -1
122
+ return na - nb
123
+ }
124
+ case "datetime": {
125
+ const ta = toTime(a)
126
+ const tb = toTime(b)
127
+ if (Number.isNaN(ta) && Number.isNaN(tb)) return 0
128
+ if (Number.isNaN(ta)) return 1
129
+ if (Number.isNaN(tb)) return -1
130
+ return ta - tb
131
+ }
132
+ case "text":
133
+ return collator.compare(defaultText(a), defaultText(b))
134
+ case "textCaseSensitive":
135
+ return collatorCase.compare(defaultText(a), defaultText(b))
136
+ case "auto":
137
+ default: {
138
+ // Arrays/Objekte: nach dem ersten Wert sortieren (siehe firstValueOf).
139
+ if (a !== null && typeof a === "object" && !(a instanceof Date)) a = firstValueOf(a)
140
+ if (b !== null && typeof b === "object" && !(b instanceof Date)) b = firstValueOf(b)
141
+ if (typeof a === "number" && typeof b === "number") return a - b
142
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b)
143
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()
144
+ return collator.compare(defaultText(a), defaultText(b))
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Sortiert eine Zeilenliste stabil nach mehreren Spalten. Leere Werte landen
151
+ * gemäß `sortUndefined` der Spalte vorn oder hinten (Default: normal vergleicht).
152
+ */
153
+ export function sortRows<R extends Row>(rows: R[], sort: SortEntry[], columns: ResolvedColumn<R>[]): R[] {
154
+ if (!sort.length) return rows
155
+ const entries = sort
156
+ .map((entry) => ({ entry, column: findColumn(columns, entry.id) }))
157
+ .filter((item): item is { entry: SortEntry; column: ResolvedColumn<R> } => Boolean(item.column))
158
+ if (!entries.length) return rows
159
+
160
+ // Index mitführen → stabile Sortierung unabhängig von der Engine.
161
+ return rows
162
+ .map((row, index) => ({ row, index }))
163
+ .sort((left, right) => {
164
+ for (const { entry, column } of entries) {
165
+ const a = column.get(left.row)
166
+ const b = column.get(right.row)
167
+ const emptyRule = column.def.sortUndefined ?? false
168
+ if (emptyRule && (isEmptyValue(a) || isEmptyValue(b))) {
169
+ if (isEmptyValue(a) && isEmptyValue(b)) continue
170
+ const emptyFirst = emptyRule === "first"
171
+ return isEmptyValue(a) ? (emptyFirst ? -1 : 1) : emptyFirst ? 1 : -1
172
+ }
173
+ const raw = column.def.comparator
174
+ ? column.def.comparator(a, b, left.row, right.row)
175
+ : compareValues(a, b, column.def.sortFn ?? "auto")
176
+ if (raw !== 0) return entry.desc ? -raw : raw
177
+ }
178
+ return left.index - right.index
179
+ })
180
+ .map((item) => item.row)
181
+ }
182
+
183
+ /**
184
+ * Nächster Sortierzustand beim Klick auf einen Spaltenkopf.
185
+ * asc → desc → aus (bzw. endlos wechselnd bei `withoutSortRemoval`),
186
+ * mit Shift mehrspaltig (begrenzt durch `maxMultiSort`, 0 = unbegrenzt).
187
+ */
188
+ export function nextSortState(
189
+ current: SortEntry[],
190
+ column: ResolvedColumn,
191
+ options: {
192
+ multi?: boolean
193
+ descFirst?: boolean
194
+ withoutRemoval?: boolean
195
+ maxMultiSort?: number
196
+ } = {}
197
+ ): SortEntry[] {
198
+ const descFirst = column.def.sortDescFirst ?? options.descFirst ?? false
199
+ const existing = current.find((entry) => entry.id === column.id)
200
+ const others = current.filter((entry) => entry.id !== column.id)
201
+
202
+ let next: SortEntry | null
203
+ if (!existing) next = { id: column.id, desc: descFirst }
204
+ else if (existing.desc === descFirst) next = { id: column.id, desc: !descFirst }
205
+ else next = options.withoutRemoval ? { id: column.id, desc: descFirst } : null
206
+
207
+ if (!options.multi) return next ? [next] : []
208
+ if (!next) return others
209
+ const combined = [...others, next]
210
+ const max = options.maxMultiSort ?? 0
211
+ return max > 0 ? combined.slice(-max) : combined
212
+ }
213
+
214
+ // ── Filter & Suche ────────────────────────────────────────────────────
215
+
216
+ function dayStart(value: unknown): number {
217
+ const time = toTime(value)
218
+ if (Number.isNaN(time)) return Number.NaN
219
+ const date = new Date(time)
220
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
221
+ }
222
+
223
+ /** Ist ein Filterwert „leer" (dann filtert die Spalte nicht)? */
224
+ export function isEmptyFilter(value: unknown): boolean {
225
+ if (value === null || value === undefined || value === "") return true
226
+ if (Array.isArray(value)) return value.every((item) => isEmptyFilter(item))
227
+ return false
228
+ }
229
+
230
+ export function matchesFilter(value: unknown, filterValue: unknown, type: FilterType): boolean {
231
+ if (isEmptyFilter(filterValue)) return true
232
+ switch (type) {
233
+ case "equals":
234
+ return defaultText(value).toLowerCase() === String(filterValue).toLowerCase()
235
+ case "number-range": {
236
+ const [min, max] = filterValue as [unknown, unknown]
237
+ const num = Number(value)
238
+ if (Number.isNaN(num)) return false
239
+ if (!isEmptyFilter(min) && num < Number(min)) return false
240
+ if (!isEmptyFilter(max) && num > Number(max)) return false
241
+ return true
242
+ }
243
+ case "date-range": {
244
+ const [from, to] = filterValue as [unknown, unknown]
245
+ const day = dayStart(value)
246
+ if (Number.isNaN(day)) return false
247
+ if (!isEmptyFilter(from) && day < dayStart(from)) return false
248
+ if (!isEmptyFilter(to) && day > dayStart(to)) return false
249
+ return true
250
+ }
251
+ case "set": {
252
+ const wanted = (filterValue as unknown[]).map((item) => String(item))
253
+ return wanted.includes(defaultText(value))
254
+ }
255
+ case "includes-any": {
256
+ const wanted = (filterValue as unknown[]).map((item) => String(item))
257
+ const actual = (Array.isArray(value) ? value : [value]).map((item) => defaultText(item))
258
+ return wanted.some((item) => actual.includes(item))
259
+ }
260
+ case "includes-all": {
261
+ const wanted = (filterValue as unknown[]).map((item) => String(item))
262
+ const actual = (Array.isArray(value) ? value : [value]).map((item) => defaultText(item))
263
+ return wanted.every((item) => actual.includes(item))
264
+ }
265
+ case "text":
266
+ default:
267
+ return defaultText(value).toLowerCase().includes(String(filterValue).toLowerCase())
268
+ }
269
+ }
270
+
271
+ export function rowMatchesFilters<R extends Row>(
272
+ row: R,
273
+ filters: FilterEntry[],
274
+ columns: ResolvedColumn<R>[]
275
+ ): boolean {
276
+ for (const filter of filters) {
277
+ if (isEmptyFilter(filter.value)) continue
278
+ const column = findColumn(columns, filter.id)
279
+ if (!column) continue
280
+ const value = column.get(row)
281
+ const ok = column.def.filterFn
282
+ ? column.def.filterFn(value, filter.value, row)
283
+ : matchesFilter(value, filter.value, column.filterType)
284
+ if (!ok) return false
285
+ }
286
+ return true
287
+ }
288
+
289
+ // Suchtext-Cache je (Spalte, Zeile): `column.text` läuft durch den Formatter
290
+ // (bei ViUR-Spalten Sprachauflösung, multiple-Joins, Format-Strings) — ohne
291
+ // Cache friert die Suche über einen großen, voll geladenen Datensatz den
292
+ // Browser ein (Zeilen × Spalten × Tastendruck). WeakMaps: der Cache lebt so
293
+ // lange wie Spalte bzw. Zeile und räumt sich selbst ab.
294
+ const searchTextCache = new WeakMap<object, WeakMap<object, string>>()
295
+
296
+ function searchTextOf<R extends Row>(column: ResolvedColumn<R>, row: R): string {
297
+ let byRow = searchTextCache.get(column as object)
298
+ if (!byRow) {
299
+ byRow = new WeakMap<object, string>()
300
+ searchTextCache.set(column as object, byRow)
301
+ }
302
+ let text = byRow.get(row as object)
303
+ if (text === undefined) {
304
+ text = column.text(row).toLowerCase()
305
+ byRow.set(row as object, text)
306
+ }
307
+ return text
308
+ }
309
+
310
+ export function rowMatchesSearch<R extends Row>(
311
+ row: R,
312
+ term: string,
313
+ columns: ResolvedColumn<R>[],
314
+ searchFn?: ((value: unknown, term: string, row: R) => boolean) | null
315
+ ): boolean {
316
+ const needle = term.trim().toLowerCase()
317
+ if (!needle) return true
318
+ return columns.some((column) => {
319
+ if (!column.searchable) return false
320
+ if (searchFn) return searchFn(column.get(row), term, row)
321
+ return searchTextOf(column, row).includes(needle)
322
+ })
323
+ }
324
+
325
+ /**
326
+ * Facetten einer Spalte: verschiedene Werte mit Trefferzahl und numerischer
327
+ * Spanne — berechnet über die Zeilen, die alle ANDEREN Filter (und die Suche)
328
+ * passieren. Genau das braucht ein set-Filter, damit er sich nicht selbst leert.
329
+ */
330
+ export function computeFacets<R extends Row>(rows: R[], column: ResolvedColumn<R>): ColumnFacets {
331
+ const counts = new Map<string, number>()
332
+ let min: number | undefined
333
+ let max: number | undefined
334
+
335
+ for (const row of rows) {
336
+ const value = column.get(row)
337
+ const items = Array.isArray(value) ? value : [value]
338
+ for (const item of items) {
339
+ if (item === null || item === undefined) continue
340
+ const text = defaultText(item)
341
+ counts.set(text, (counts.get(text) ?? 0) + 1)
342
+ const num = Number(item)
343
+ if (!Number.isNaN(num) && item !== "") {
344
+ min = min === undefined ? num : Math.min(min, num)
345
+ max = max === undefined ? num : Math.max(max, num)
346
+ }
347
+ }
348
+ }
349
+
350
+ const values = [...counts.entries()]
351
+ .map(([value, count]) => ({ value, count }))
352
+ .sort((a, b) => b.count - a.count || collator.compare(a.value, b.value))
353
+
354
+ return { values, min, max }
355
+ }
356
+
357
+ // ── Aggregation (Gruppenzeilen) ───────────────────────────────────────
358
+
359
+ function numbers(values: unknown[]): number[] {
360
+ return values.map((value) => Number(value)).filter((value) => !Number.isNaN(value))
361
+ }
362
+
363
+ export function aggregate(kind: AggregationKind, values: unknown[]): unknown {
364
+ const nums = numbers(values)
365
+ switch (kind) {
366
+ case "sum":
367
+ return nums.reduce((total, value) => total + value, 0)
368
+ case "min":
369
+ return nums.length ? Math.min(...nums) : undefined
370
+ case "max":
371
+ return nums.length ? Math.max(...nums) : undefined
372
+ case "extent":
373
+ return nums.length ? [Math.min(...nums), Math.max(...nums)] : undefined
374
+ case "mean":
375
+ return nums.length ? nums.reduce((total, value) => total + value, 0) / nums.length : undefined
376
+ case "median": {
377
+ if (!nums.length) return undefined
378
+ const sorted = [...nums].sort((a, b) => a - b)
379
+ const middle = Math.floor(sorted.length / 2)
380
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2
381
+ }
382
+ case "count":
383
+ return values.length
384
+ case "unique":
385
+ return [...new Set(values.map((value) => defaultText(value)))]
386
+ case "uniqueCount":
387
+ return new Set(values.map((value) => defaultText(value))).size
388
+ default:
389
+ return undefined
390
+ }
391
+ }
392
+
393
+ export function aggregateColumn<R extends Row>(column: ResolvedColumn<R>, rows: R[]): unknown {
394
+ const kind = column.def.aggregation
395
+ if (!kind) return undefined
396
+ const values = rows.map((row) => column.get(row))
397
+ return typeof kind === "function" ? kind(values, rows) : aggregate(kind, values)
398
+ }
399
+
400
+ export { defaultText }
@@ -0,0 +1,60 @@
1
+ // Öffentliche Oberfläche des DataTable-Widgets.
2
+ export { default as DataTable } from "./DataTable.vue"
3
+ export { default as ColumnFilterPanel } from "./ColumnFilterPanel.vue"
4
+ export * from "./types"
5
+ export {
6
+ aggregate,
7
+ aggregateColumn,
8
+ compareValues,
9
+ computeFacets,
10
+ defaultText,
11
+ findColumn,
12
+ firstValueOf,
13
+ getPath,
14
+ isEmptyFilter,
15
+ isEmptyValue,
16
+ matchesFilter,
17
+ nextSortState,
18
+ resolveColumns,
19
+ rowMatchesFilters,
20
+ rowMatchesSearch,
21
+ sortRows,
22
+ } from "./columns"
23
+ export {
24
+ buildRows,
25
+ collectKeys,
26
+ flattenNodes,
27
+ isDescendant,
28
+ locateNode,
29
+ nestByParent,
30
+ orderValueFor,
31
+ paginateNodes,
32
+ selectionStateFor,
33
+ type BuildRowsOptions,
34
+ type BuiltRows,
35
+ type ChildRowsAccessor,
36
+ type NestedTree,
37
+ type NodeLocation,
38
+ type OrderValue,
39
+ type SelectionState,
40
+ } from "./rows"
41
+ export { useColumnLayout, type ColumnLayout, type LayoutColumn } from "./useColumnLayout"
42
+ export { useGridNavigation } from "./useGridNavigation"
43
+ export { useVirtualRows } from "./useVirtualRows"
44
+ export {
45
+ badgesForBone,
46
+ boneFamily,
47
+ boneText,
48
+ columnsFromStructure,
49
+ selectToneMap,
50
+ POSITIVE_KEYWORDS,
51
+ NEGATIVE_KEYWORDS,
52
+ type BoneDescription,
53
+ type BoneStructure,
54
+ type ColumnsFromStructureOptions,
55
+ } from "./viurColumns"
56
+ // ViurDataTable ist (wie ViurBrowser) ein JS-SFC — seine Typen liegen in
57
+ // ViurDataTable.d.vue.ts (Import: ".../DataTable/ViurDataTable.d.vue", wie
58
+ // bei ViForm).
59
+ // @ts-expect-error — JS-SFC ohne eingebettetes Typing
60
+ export { default as ViurDataTable } from "./ViurDataTable.vue"