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.
- package/README.md +79 -0
- package/index.d.ts +351 -0
- package/index.js +46 -0
- package/package.json +84 -0
- package/src/components/Browser/Browser.vue +496 -0
- package/src/components/Browser/TreeNav.vue +408 -0
- package/src/components/Browser/ViurBrowser.d.vue.ts +64 -0
- package/src/components/Browser/ViurBrowser.vue +271 -0
- package/src/components/Browser/dragSession.ts +134 -0
- package/src/components/Browser/index.ts +14 -0
- package/src/components/Browser/types.ts +30 -0
- package/src/components/DataTable/ColumnFilterPanel.vue +225 -0
- package/src/components/DataTable/DataTable.vue +3155 -0
- package/src/components/DataTable/ViurDataTable.d.vue.ts +72 -0
- package/src/components/DataTable/ViurDataTable.vue +331 -0
- package/src/components/DataTable/columns.ts +400 -0
- package/src/components/DataTable/index.ts +60 -0
- package/src/components/DataTable/rows.ts +446 -0
- package/src/components/DataTable/types.ts +267 -0
- package/src/components/DataTable/useColumnLayout.ts +294 -0
- package/src/components/DataTable/useGridNavigation.ts +193 -0
- package/src/components/DataTable/useVirtualRows.ts +126 -0
- package/src/components/DataTable/viurColumns.ts +330 -0
- package/src/forms/ViForm.d.vue.ts +106 -0
- package/src/forms/ViForm.vue +592 -0
- package/src/forms/ViFormCategory.vue +57 -0
- package/src/forms/bones/Bone.vue +1023 -0
- package/src/forms/bones/BoneActions.vue +127 -0
- package/src/forms/bones/BoneLabel.vue +122 -0
- package/src/forms/bones/BoneMultipleList.vue +91 -0
- package/src/forms/bones/BoneMultipleWrapper.vue +105 -0
- package/src/forms/bones/BoneNestedWrapper.vue +155 -0
- package/src/forms/bones/BoneSingleEntry.vue +51 -0
- package/src/forms/bones/FilterBone.vue +694 -0
- package/src/forms/bones/actionbar/defaultBar.vue +98 -0
- package/src/forms/bones/actionbar/fileBar.vue +165 -0
- package/src/forms/bones/actionbar/relationalBar.vue +125 -0
- package/src/forms/bones/actions/ActionDialog.vue +70 -0
- package/src/forms/bones/actions/Debug.vue +106 -0
- package/src/forms/bones/actions/Delete.vue +13 -0
- package/src/forms/bones/actions/DescribeImage.vue +201 -0
- package/src/forms/bones/actions/Download.vue +27 -0
- package/src/forms/bones/actions/Placeholder.vue +14 -0
- package/src/forms/bones/actions/Translation.vue +238 -0
- package/src/forms/bones/actions/index.js +280 -0
- package/src/forms/bones/bone.d.ts +45 -0
- package/src/forms/bones/country_information.json +779 -0
- package/src/forms/bones/index.js +225 -0
- package/src/forms/bones/uploadAdapter.js +46 -0
- package/src/forms/bones/utils.js +284 -0
- package/src/forms/bones/view/base_item.vue +35 -0
- package/src/forms/bones/view/boneLogic.js +202 -0
- package/src/forms/bones/view/index.js +5 -0
- package/src/forms/bones/widgets/booleanBone.vue +83 -0
- package/src/forms/bones/widgets/booleanBoneChoose.vue +57 -0
- package/src/forms/bones/widgets/booleanBoneSelect.vue +58 -0
- package/src/forms/bones/widgets/codeBone.vue +214 -0
- package/src/forms/bones/widgets/colorBone.vue +75 -0
- package/src/forms/bones/widgets/dateBone.vue +114 -0
- package/src/forms/bones/widgets/emailBone.vue +100 -0
- package/src/forms/bones/widgets/fileBone.vue +273 -0
- package/src/forms/bones/widgets/jsonBone.vue +63 -0
- package/src/forms/bones/widgets/keyBone.vue +71 -0
- package/src/forms/bones/widgets/numericBone.vue +125 -0
- package/src/forms/bones/widgets/passwordBone.vue +215 -0
- package/src/forms/bones/widgets/phoneBone.vue +173 -0
- package/src/forms/bones/widgets/rawBone.vue +94 -0
- package/src/forms/bones/widgets/recordBone.vue +64 -0
- package/src/forms/bones/widgets/relationalBone.vue +164 -0
- package/src/forms/bones/widgets/relationalBoneSelect.vue +120 -0
- package/src/forms/bones/widgets/selectBone.vue +122 -0
- package/src/forms/bones/widgets/selectBoneChoose.vue +104 -0
- package/src/forms/bones/widgets/spatialBone.vue +111 -0
- package/src/forms/bones/widgets/stringBone.vue +139 -0
- package/src/forms/bones/widgets/textBone.vue +641 -0
- package/src/forms/install.d.ts +4 -0
- package/src/forms/install.js +43 -0
- package/src/forms/layouts/LayloutList2Columns.vue +57 -0
- package/src/forms/layouts/LayoutCategory.vue +38 -0
- package/src/forms/layouts/LayoutList.vue +31 -0
- package/src/forms/utils.js +601 -0
- package/src/public/assets/icons/fontawesome-manifest.json +8 -0
- package/src/public/assets/icons/regular/file.svg +1 -0
- package/src/public/assets/icons/solid/arrows-left-right.svg +1 -0
- package/src/public/assets/icons/solid/bug.svg +1 -0
- package/src/public/assets/icons/solid/check.svg +1 -0
- package/src/public/assets/icons/solid/circle-info.svg +1 -0
- package/src/public/assets/icons/solid/compress.svg +1 -0
- package/src/public/assets/icons/solid/download.svg +1 -0
- package/src/public/assets/icons/solid/expand.svg +1 -0
- package/src/public/assets/icons/solid/grip-vertical.svg +1 -0
- package/src/public/assets/icons/solid/plus.svg +1 -0
- package/src/public/assets/icons/solid/question.svg +1 -0
- package/src/public/assets/icons/solid/triangle-exclamation.svg +1 -0
- package/src/public/assets/icons/solid/upload.svg +1 -0
- package/src/public/assets/icons/solid/xmark.svg +1 -0
- package/src/request/cache.js +183 -0
- package/src/request/configStore.js +516 -0
- package/src/request/envelope.js +211 -0
- package/src/request/index.js +35 -0
- package/src/request/messageStore.js +201 -0
- package/src/request/request.js +752 -0
- package/src/request/skel.js +90 -0
- package/src/theme/colorStore.js +168 -0
- package/src/translations/de.js +178 -0
- package/src/translations/en.js +177 -0
- package/src/user/LoginScreen.d.vue.ts +54 -0
- package/src/user/LoginScreen.vue +627 -0
- package/src/user/google.js +75 -0
- package/src/user/index.js +3 -0
- package/src/user/store.d.ts +59 -0
- package/src/user/store.js +183 -0
- package/src/webawesomeCompat.css +72 -0
- 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"
|