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,267 @@
|
|
|
1
|
+
// Typen des DataTable-Widgets — nachgebaut nach <wa-data-grid> (WebAwesome Pro
|
|
2
|
+
// v12), aber mit Vue-Mitteln: Zell-Inhalte kommen über Slots statt über
|
|
3
|
+
// Lit-Templates, Zustand über v-model statt über Properties am Element.
|
|
4
|
+
|
|
5
|
+
export type Row = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
export type Align = "start" | "center" | "end"
|
|
8
|
+
export type PinSide = "start" | "end"
|
|
9
|
+
|
|
10
|
+
export type SortMode =
|
|
11
|
+
| "auto" // Zahlen numerisch, Datum chronologisch, sonst alphanumerisch
|
|
12
|
+
| "text"
|
|
13
|
+
| "textCaseSensitive"
|
|
14
|
+
| "number"
|
|
15
|
+
| "datetime"
|
|
16
|
+
| "basic"
|
|
17
|
+
|
|
18
|
+
export type FilterType =
|
|
19
|
+
| "text" // Teilstring, case-insensitive (Default)
|
|
20
|
+
| "equals"
|
|
21
|
+
| "number-range" // [min, max]
|
|
22
|
+
| "date-range" // [von, bis], inklusive nach Kalendertag
|
|
23
|
+
| "set" // Wert ist einer der gewählten
|
|
24
|
+
| "includes-any" // Array-Zelle enthält einen der gewählten
|
|
25
|
+
| "includes-all" // Array-Zelle enthält alle gewählten
|
|
26
|
+
|
|
27
|
+
export type AggregationKind = "sum" | "min" | "max" | "extent" | "mean" | "median" | "count" | "unique" | "uniqueCount"
|
|
28
|
+
|
|
29
|
+
export interface FilterOption {
|
|
30
|
+
value: string
|
|
31
|
+
label?: string
|
|
32
|
+
count?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Eine Spaltendefinition. Reines Datenobjekt — reaktiv, ohne Render-Logik. */
|
|
36
|
+
export interface DataTableColumn<R extends Row = Row> {
|
|
37
|
+
/** Pfad in die Zeile, Punktpfade erlaubt ("user.name"). Dient als id, wenn `id` fehlt. */
|
|
38
|
+
field?: string
|
|
39
|
+
/** Explizite id. Pflicht für Spalten ohne `field` (Aktionen, berechnete Spalten). */
|
|
40
|
+
id?: string
|
|
41
|
+
/**
|
|
42
|
+
* Berechnet den Zellwert aus der Zeile. Anders als `formatter` (nur Darstellung)
|
|
43
|
+
* geht dieser Wert in Sortierung, Filter, Suche und Export ein.
|
|
44
|
+
*/
|
|
45
|
+
value?: (row: R) => unknown
|
|
46
|
+
label?: string
|
|
47
|
+
align?: Align
|
|
48
|
+
headerAlign?: Align
|
|
49
|
+
/** Default: true, sobald `field` oder `value` vorhanden ist. */
|
|
50
|
+
sortable?: boolean
|
|
51
|
+
sortFn?: SortMode
|
|
52
|
+
/** Wohin leere Werte sortieren. */
|
|
53
|
+
sortUndefined?: false | "first" | "last"
|
|
54
|
+
/** Erste Sortierung dieser Spalte absteigend. */
|
|
55
|
+
sortDescFirst?: boolean
|
|
56
|
+
comparator?: (a: unknown, b: unknown, rowA: R, rowB: R) => number
|
|
57
|
+
/** Default: true, sobald `field` oder `value` vorhanden ist. */
|
|
58
|
+
searchable?: boolean
|
|
59
|
+
filterable?: boolean
|
|
60
|
+
filterType?: FilterType
|
|
61
|
+
filterFn?: (value: unknown, filterValue: unknown, row: R) => boolean
|
|
62
|
+
/** Ersetzt die berechneten Facetten im set/includes-Filter (z. B. im Server-Modus). */
|
|
63
|
+
filterOptions?: FilterOption[]
|
|
64
|
+
hidden?: boolean
|
|
65
|
+
hideable?: boolean
|
|
66
|
+
resizable?: boolean
|
|
67
|
+
movable?: boolean
|
|
68
|
+
pinnable?: boolean
|
|
69
|
+
pinned?: PinSide
|
|
70
|
+
width?: number
|
|
71
|
+
minWidth?: number
|
|
72
|
+
maxWidth?: number
|
|
73
|
+
/** Anteil am freien Platz; schlägt `width`. */
|
|
74
|
+
flex?: number
|
|
75
|
+
/** Zelltext. Für reichen Inhalt den Slot `cell:<id>` verwenden. */
|
|
76
|
+
formatter?: (value: unknown, row: R) => string
|
|
77
|
+
/**
|
|
78
|
+
* Wert(e) als farbige Plakette(n) statt als Text rendern. Positive Werte
|
|
79
|
+
* grün, negative rot, alles dazwischen regenbogen-abgestuft — siehe
|
|
80
|
+
* `badgesForBone` in viurColumns.ts, das diese Funktion für reine
|
|
81
|
+
* select- und bool-Bones erzeugt.
|
|
82
|
+
*/
|
|
83
|
+
badge?: (value: unknown, row: R) => CellBadge[] | null
|
|
84
|
+
/** Fußzeile: Text oder Funktion über die gefilterten Zeilen (alle Seiten). */
|
|
85
|
+
footer?: string | ((rows: R[]) => string)
|
|
86
|
+
aggregation?: AggregationKind | ((values: unknown[], rows: R[]) => unknown)
|
|
87
|
+
cellClass?: string | ((value: unknown, row: R) => string)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Spalte mit aufgelösten Defaults — was die Komponente intern benutzt. */
|
|
91
|
+
/** Eine farbige Zell-Plakette (siehe DataTableColumn.badge). */
|
|
92
|
+
export interface CellBadge {
|
|
93
|
+
label: string
|
|
94
|
+
/** CSS-Farbe (Basiston); Hintergrund/Rand mischt die Tabelle selbst. */
|
|
95
|
+
color: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ResolvedColumn<R extends Row = Row> {
|
|
99
|
+
id: string
|
|
100
|
+
def: DataTableColumn<R>
|
|
101
|
+
field?: string
|
|
102
|
+
label: string
|
|
103
|
+
align: Align
|
|
104
|
+
headerAlign: Align
|
|
105
|
+
sortable: boolean
|
|
106
|
+
searchable: boolean
|
|
107
|
+
filterable: boolean
|
|
108
|
+
filterType: FilterType
|
|
109
|
+
hideable: boolean
|
|
110
|
+
/** Zellwert einer Zeile (value() oder Punktpfad). */
|
|
111
|
+
get: (row: R) => unknown
|
|
112
|
+
/** Zelltext für Anzeige, Suche als Text, Kopieren und CSV. */
|
|
113
|
+
text: (row: R) => string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SortEntry {
|
|
117
|
+
id: string
|
|
118
|
+
desc: boolean
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface FilterEntry {
|
|
122
|
+
id: string
|
|
123
|
+
value: unknown
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ColumnFacets {
|
|
127
|
+
/** Verschiedene Werte mit Trefferzahl, absteigend nach Häufigkeit. */
|
|
128
|
+
values: { value: string; count: number }[]
|
|
129
|
+
min?: number
|
|
130
|
+
max?: number
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Anfrage an den Server (dataSource / data-request). */
|
|
134
|
+
export interface DataTableRequest {
|
|
135
|
+
sort: SortEntry[]
|
|
136
|
+
filters: FilterEntry[]
|
|
137
|
+
search: string
|
|
138
|
+
page: number
|
|
139
|
+
pageSize: number
|
|
140
|
+
signal: AbortSignal
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface DataTableResponse<R extends Row = Row> {
|
|
144
|
+
rows: R[]
|
|
145
|
+
total: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Serialisierbarer Schnappschuss des vom Nutzer verstellbaren Zustands. */
|
|
149
|
+
export interface DataTableState {
|
|
150
|
+
version: 1
|
|
151
|
+
columnOrder?: string[]
|
|
152
|
+
columnWidths?: Record<string, number>
|
|
153
|
+
columnVisibility?: Record<string, boolean>
|
|
154
|
+
columnPinning?: { start: string[]; end: string[] }
|
|
155
|
+
sort?: SortEntry[]
|
|
156
|
+
filters?: FilterEntry[]
|
|
157
|
+
search?: string
|
|
158
|
+
selectedKeys?: RowKey[]
|
|
159
|
+
expandedKeys?: RowKey[]
|
|
160
|
+
page?: number
|
|
161
|
+
pageSize?: number
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type RowKey = string | number
|
|
165
|
+
|
|
166
|
+
/** Knoten der (Baum-/Gruppen-)Hierarchie nach Filter und Sortierung. */
|
|
167
|
+
export interface RowNode<R extends Row = Row> {
|
|
168
|
+
kind: "data" | "group"
|
|
169
|
+
key: RowKey
|
|
170
|
+
depth: number
|
|
171
|
+
children: RowNode<R>[]
|
|
172
|
+
/** kind "data" */
|
|
173
|
+
row?: R
|
|
174
|
+
/** kind "group" */
|
|
175
|
+
groupColumnId?: string
|
|
176
|
+
groupValue?: unknown
|
|
177
|
+
groupCount?: number
|
|
178
|
+
groupRows?: R[]
|
|
179
|
+
aggregates?: Record<string, unknown>
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Eine gerenderte Zeile (flach, inklusive Detail-Panels). */
|
|
183
|
+
export interface DisplayRow<R extends Row = Row> {
|
|
184
|
+
kind: "data" | "group" | "detail"
|
|
185
|
+
/** Eindeutig im Render-Durchlauf (Detailzeilen haben ein Suffix). */
|
|
186
|
+
key: string
|
|
187
|
+
/** Schlüssel der zugehörigen Zeile/Gruppe. */
|
|
188
|
+
nodeKey: RowKey
|
|
189
|
+
depth: number
|
|
190
|
+
expandable: boolean
|
|
191
|
+
expanded: boolean
|
|
192
|
+
/** Laufender Index der Datenzeilen (nur kind "data"). */
|
|
193
|
+
index: number
|
|
194
|
+
/**
|
|
195
|
+
* Die DURCHLAUF-Spalten links des eigenen Winkels (Länge = `depth - 1`).
|
|
196
|
+
* `true` heißt: dort läuft eine senkrechte Linie durch, weil der Vorfahre
|
|
197
|
+
* dieser Spalte noch ein Geschwister nach sich hat.
|
|
198
|
+
*
|
|
199
|
+
* Wichtig ist die Zuordnung: die Linie in einer Spalte verbindet nicht den
|
|
200
|
+
* Vorfahren DIESER Ebene mit seinem Geschwister, sondern den Vorfahren der
|
|
201
|
+
* NÄCHSTEN Ebene — genau wie in `tree`-Ausgaben:
|
|
202
|
+
*
|
|
203
|
+
* ```
|
|
204
|
+
* A
|
|
205
|
+
* ├── B
|
|
206
|
+
* │ └── C ← das "│" steht hier, weil nach B noch D kommt
|
|
207
|
+
* └── D
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* Wurzeln erzeugen keine Durchlauf-Spalte: zwischen zwei Wurzeln wird keine
|
|
211
|
+
* Linie gezogen.
|
|
212
|
+
*/
|
|
213
|
+
ancestorLines: boolean[]
|
|
214
|
+
/** Letztes Kind seines Elternteils → Winkel „└" statt „├". */
|
|
215
|
+
isLastChild: boolean
|
|
216
|
+
row?: R
|
|
217
|
+
node?: RowNode<R>
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Die per `defineExpose` freigegebene API der Komponente. Als Typ für
|
|
222
|
+
* Template-Refs verwenden — `InstanceType<typeof DataTable>` funktioniert bei
|
|
223
|
+
* generischen Komponenten nicht.
|
|
224
|
+
*/
|
|
225
|
+
export interface DataTableApi<R extends Row = Row> {
|
|
226
|
+
getVisibleRows: () => R[]
|
|
227
|
+
getProcessedRows: () => R[]
|
|
228
|
+
getSelectedRows: () => R[]
|
|
229
|
+
scrollToIndex: (index: number, align?: "start" | "center" | "end") => void
|
|
230
|
+
expandRow: (key: RowKey) => void
|
|
231
|
+
collapseRow: (key: RowKey) => void
|
|
232
|
+
expandAllRows: () => void
|
|
233
|
+
collapseAllRows: () => void
|
|
234
|
+
toggleColumn: (id: string, visible: boolean) => void
|
|
235
|
+
pinColumn: (id: string, side: PinSide | false) => void
|
|
236
|
+
getColumnPin: (id: string) => PinSide | false
|
|
237
|
+
autoSizeColumn: (id: string) => void
|
|
238
|
+
autoSizeColumns: () => void
|
|
239
|
+
sizeColumnsToFit: () => void
|
|
240
|
+
resetColumns: () => void
|
|
241
|
+
getColumnFacets: (id: string) => ColumnFacets
|
|
242
|
+
reload: () => void
|
|
243
|
+
/** Nachgeladene Kinder einer Zeile verwerfen und erneut holen. */
|
|
244
|
+
reloadChildren: (key: RowKey) => Promise<void>
|
|
245
|
+
/** Nachgeladene Kinder einer Zeile (undefined = noch nicht geladen). */
|
|
246
|
+
getLoadedChildren: (key: RowKey) => R[] | undefined
|
|
247
|
+
/** Nachgeladene Kinder einer Zeile ersetzen, ohne zu laden. */
|
|
248
|
+
setLoadedChildren: (key: RowKey, rows: R[]) => void
|
|
249
|
+
getDataAsCsv: (options?: {
|
|
250
|
+
columnIds?: string[]
|
|
251
|
+
includeHeaders?: boolean
|
|
252
|
+
delimiter?: string
|
|
253
|
+
escapeFormulas?: boolean
|
|
254
|
+
}) => string
|
|
255
|
+
exportDataAsCsv: (options?: {
|
|
256
|
+
fileName?: string
|
|
257
|
+
columnIds?: string[]
|
|
258
|
+
includeHeaders?: boolean
|
|
259
|
+
delimiter?: string
|
|
260
|
+
escapeFormulas?: boolean
|
|
261
|
+
}) => void
|
|
262
|
+
copySelectedRows: (options?: { format?: "tsv" | "csv" }) => Promise<number>
|
|
263
|
+
getState: () => DataTableState
|
|
264
|
+
setState: (state: Partial<DataTableState>) => void
|
|
265
|
+
resetState: () => void
|
|
266
|
+
focus: () => void
|
|
267
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Spalten-Layout: Reihenfolge, Breiten, Sichtbarkeit, Pinning — plus die
|
|
2
|
+
// Zeigergesten für Resize und Reorder. Alles in EINEM reaktiven State-Objekt
|
|
3
|
+
// (analog zur Komponenten-Regel), damit die Komponente nur noch rendert.
|
|
4
|
+
|
|
5
|
+
import { computed, reactive } from "vue"
|
|
6
|
+
import type { PinSide, ResolvedColumn, Row } from "./types"
|
|
7
|
+
|
|
8
|
+
const DEFAULT_WIDTH = 160
|
|
9
|
+
const DEFAULT_MIN_WIDTH = 64
|
|
10
|
+
|
|
11
|
+
export interface LayoutColumn<R extends Row = Row> {
|
|
12
|
+
column: ResolvedColumn<R>
|
|
13
|
+
/** Index in der sichtbaren Reihenfolge. */
|
|
14
|
+
index: number
|
|
15
|
+
/** Effektive Breite in px (gemessen oder gesetzt). */
|
|
16
|
+
width: number
|
|
17
|
+
/** Spaltenbreite für grid-template-columns. */
|
|
18
|
+
track: string
|
|
19
|
+
pinned: PinSide | false
|
|
20
|
+
/** Sticky-Abstand zur jeweiligen Kante (px). */
|
|
21
|
+
offset: number
|
|
22
|
+
/** Letzte am Start bzw. erste am Ende gepinnte Spalte (für die Trennlinie). */
|
|
23
|
+
pinEdge: boolean
|
|
24
|
+
resizable: boolean
|
|
25
|
+
movable: boolean
|
|
26
|
+
pinnable: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ColumnLayoutOptions<R extends Row = Row> {
|
|
30
|
+
columns: () => ResolvedColumn<R>[]
|
|
31
|
+
resizable?: () => boolean
|
|
32
|
+
reorderable?: () => boolean
|
|
33
|
+
pinnable?: () => boolean
|
|
34
|
+
onResize?: (id: string, width: number, finished: boolean) => void
|
|
35
|
+
onMove?: (order: string[], finished: boolean) => void
|
|
36
|
+
onPin?: (id: string, side: PinSide | false) => void
|
|
37
|
+
onVisibilityChange?: (id: string, visible: boolean) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clampWidth<R extends Row>(column: ResolvedColumn<R>, width: number): number {
|
|
41
|
+
const min = column.def.minWidth ?? DEFAULT_MIN_WIDTH
|
|
42
|
+
const max = column.def.maxWidth ?? Number.POSITIVE_INFINITY
|
|
43
|
+
return Math.max(min, Math.min(max, width))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOptions<R>) {
|
|
47
|
+
const state = reactive({
|
|
48
|
+
/** Vom Nutzer gesetzte Reihenfolge (ids). Leer = Reihenfolge der Definitionen. */
|
|
49
|
+
order: [] as string[],
|
|
50
|
+
/** Vom Nutzer gezogene Breiten. */
|
|
51
|
+
widths: {} as Record<string, number>,
|
|
52
|
+
/** Sichtbarkeit; fehlt ein Eintrag, gilt `hidden` aus der Definition. */
|
|
53
|
+
visibility: {} as Record<string, boolean>,
|
|
54
|
+
/** Pinning; fehlt ein Eintrag, gilt `pinned` aus der Definition. */
|
|
55
|
+
pinning: {} as Record<string, PinSide | false>,
|
|
56
|
+
/** Im DOM gemessene Breiten — Grundlage der Sticky-Abstände. */
|
|
57
|
+
measured: {} as Record<string, number>,
|
|
58
|
+
/** Laufende Resize-Geste. */
|
|
59
|
+
resizing: null as { id: string; startX: number; startWidth: number } | null,
|
|
60
|
+
/** Laufende Reorder-Geste. */
|
|
61
|
+
dragging: null as { id: string; overId: string | null; x: number; y: number } | null,
|
|
62
|
+
|
|
63
|
+
ordered: computed((): ResolvedColumn<R>[] => {
|
|
64
|
+
const columns = options.columns()
|
|
65
|
+
if (!state.order.length) return columns
|
|
66
|
+
const byId = new Map(columns.map((column) => [column.id, column]))
|
|
67
|
+
const sorted: ResolvedColumn<R>[] = []
|
|
68
|
+
for (const id of state.order) {
|
|
69
|
+
const column = byId.get(id)
|
|
70
|
+
if (column) {
|
|
71
|
+
sorted.push(column)
|
|
72
|
+
byId.delete(id)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Neue Spalten, die die gespeicherte Reihenfolge nicht kennt, hinten dran.
|
|
76
|
+
return [...sorted, ...byId.values()]
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
/** Sichtbare Spalten in Anzeigereihenfolge: gepinnt (Start) → frei → gepinnt (Ende). */
|
|
80
|
+
visibleColumns: computed((): LayoutColumn<R>[] => {
|
|
81
|
+
const visible = state.ordered.filter((column: ResolvedColumn<R>) => isVisible(column))
|
|
82
|
+
const pinnedStart = visible.filter((column: ResolvedColumn<R>) => pinOf(column) === "start")
|
|
83
|
+
const pinnedEnd = visible.filter((column: ResolvedColumn<R>) => pinOf(column) === "end")
|
|
84
|
+
const free = visible.filter((column: ResolvedColumn<R>) => !pinOf(column))
|
|
85
|
+
const arranged = [...pinnedStart, ...free, ...pinnedEnd]
|
|
86
|
+
|
|
87
|
+
let startOffset = 0
|
|
88
|
+
const offsets = new Map<string, number>()
|
|
89
|
+
for (const column of pinnedStart) {
|
|
90
|
+
offsets.set(column.id, startOffset)
|
|
91
|
+
startOffset += widthOf(column)
|
|
92
|
+
}
|
|
93
|
+
let endOffset = 0
|
|
94
|
+
for (const column of [...pinnedEnd].reverse()) {
|
|
95
|
+
offsets.set(column.id, endOffset)
|
|
96
|
+
endOffset += widthOf(column)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return arranged.map((column, index) => {
|
|
100
|
+
const pinned = pinOf(column)
|
|
101
|
+
return {
|
|
102
|
+
column,
|
|
103
|
+
index,
|
|
104
|
+
width: widthOf(column),
|
|
105
|
+
track: trackOf(column),
|
|
106
|
+
pinned,
|
|
107
|
+
offset: offsets.get(column.id) ?? 0,
|
|
108
|
+
pinEdge:
|
|
109
|
+
(pinned === "start" && column === pinnedStart[pinnedStart.length - 1]) ||
|
|
110
|
+
(pinned === "end" && column === pinnedEnd[0]),
|
|
111
|
+
resizable: column.def.resizable ?? options.resizable?.() ?? false,
|
|
112
|
+
movable: column.def.movable ?? options.reorderable?.() ?? false,
|
|
113
|
+
pinnable: column.def.pinnable ?? options.pinnable?.() ?? false,
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
hiddenColumns: computed((): ResolvedColumn<R>[] =>
|
|
119
|
+
state.ordered.filter((column: ResolvedColumn<R>) => !isVisible(column))
|
|
120
|
+
),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
function isVisible(column: ResolvedColumn<R>): boolean {
|
|
124
|
+
return state.visibility[column.id] ?? !column.def.hidden
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pinOf(column: ResolvedColumn<R>): PinSide | false {
|
|
128
|
+
return state.pinning[column.id] ?? column.def.pinned ?? false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Effektive Breite: gezogen > gemessen > definiert > Default. */
|
|
132
|
+
function widthOf(column: ResolvedColumn<R>): number {
|
|
133
|
+
const width = state.widths[column.id] ?? state.measured[column.id] ?? column.def.width ?? DEFAULT_WIDTH
|
|
134
|
+
return clampWidth(column, width)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function trackOf(column: ResolvedColumn<R>): string {
|
|
138
|
+
// Eine vom Nutzer gezogene Breite gewinnt IMMER — auch gegen flex, sonst
|
|
139
|
+
// ließe sich eine flex-Spalte nicht von Hand skalieren.
|
|
140
|
+
const dragged = state.widths[column.id]
|
|
141
|
+
if (dragged !== undefined) return `${clampWidth(column, dragged)}px`
|
|
142
|
+
const fixed = column.def.width
|
|
143
|
+
if (fixed !== undefined && !column.def.flex) return `${clampWidth(column, fixed)}px`
|
|
144
|
+
const min = column.def.minWidth ?? DEFAULT_MIN_WIDTH
|
|
145
|
+
const flex = column.def.flex ?? 1
|
|
146
|
+
const max = column.def.maxWidth
|
|
147
|
+
const upper = max ? `${max}px` : `${flex}fr`
|
|
148
|
+
return `minmax(${min}px, ${upper})`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function setMeasured(id: string, width: number): void {
|
|
152
|
+
if (Math.abs((state.measured[id] ?? 0) - width) < 0.5) return
|
|
153
|
+
state.measured[id] = width
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Sichtbarkeit ────────────────────────────────────────────────────
|
|
157
|
+
function toggleColumn(id: string, visible?: boolean, silent = false): void {
|
|
158
|
+
const column = options.columns().find((item) => item.id === id)
|
|
159
|
+
if (!column) return
|
|
160
|
+
const next = visible ?? !isVisible(column)
|
|
161
|
+
state.visibility[id] = next
|
|
162
|
+
if (!silent) options.onVisibilityChange?.(id, next)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Pinning ─────────────────────────────────────────────────────────
|
|
166
|
+
function pinColumn(id: string, side: PinSide | false, silent = false): void {
|
|
167
|
+
// Ohne feste Breite die aktuelle Breite einfrieren — sonst springt die
|
|
168
|
+
// Spalte beim Pinnen, weil der Sticky-Abstand eine px-Breite braucht.
|
|
169
|
+
const column = options.columns().find((item) => item.id === id)
|
|
170
|
+
if (column && side && state.widths[id] === undefined && column.def.width === undefined) {
|
|
171
|
+
state.widths[id] = widthOf(column)
|
|
172
|
+
}
|
|
173
|
+
state.pinning[id] = side
|
|
174
|
+
if (!silent) options.onPin?.(id, side)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getColumnPin(id: string): PinSide | false {
|
|
178
|
+
const column = options.columns().find((item) => item.id === id)
|
|
179
|
+
return column ? pinOf(column) : false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Reihenfolge ─────────────────────────────────────────────────────
|
|
183
|
+
function currentOrder(): string[] {
|
|
184
|
+
return state.ordered.map((column: ResolvedColumn<R>) => column.id)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function moveColumn(id: string, targetId: string, finished = true): void {
|
|
188
|
+
if (id === targetId) return
|
|
189
|
+
const order = currentOrder()
|
|
190
|
+
const from = order.indexOf(id)
|
|
191
|
+
const to = order.indexOf(targetId)
|
|
192
|
+
if (from === -1 || to === -1) return
|
|
193
|
+
order.splice(to, 0, ...order.splice(from, 1))
|
|
194
|
+
state.order = order
|
|
195
|
+
options.onMove?.(order, finished)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Spalte um `delta` Positionen verschieben (Tastatur: Shift + ←/→). */
|
|
199
|
+
function shiftColumn(id: string, delta: number): void {
|
|
200
|
+
const visible = state.visibleColumns.map((item: LayoutColumn<R>) => item.column.id)
|
|
201
|
+
const index = visible.indexOf(id)
|
|
202
|
+
const target = visible[index + delta]
|
|
203
|
+
if (target) moveColumn(id, target, true)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Resize ──────────────────────────────────────────────────────────
|
|
207
|
+
function startResize(id: string, clientX: number): void {
|
|
208
|
+
const column = options.columns().find((item) => item.id === id)
|
|
209
|
+
if (!column) return
|
|
210
|
+
state.resizing = { id, startX: clientX, startWidth: widthOf(column) }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function moveResize(clientX: number): void {
|
|
214
|
+
const active = state.resizing
|
|
215
|
+
if (!active) return
|
|
216
|
+
const column = options.columns().find((item) => item.id === active.id)
|
|
217
|
+
if (!column) return
|
|
218
|
+
const width = clampWidth(column, active.startWidth + (clientX - active.startX))
|
|
219
|
+
state.widths[active.id] = width
|
|
220
|
+
options.onResize?.(active.id, width, false)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function endResize(): void {
|
|
224
|
+
const active = state.resizing
|
|
225
|
+
state.resizing = null
|
|
226
|
+
if (active) options.onResize?.(active.id, state.widths[active.id] ?? 0, true)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function setWidth(id: string, width: number): void {
|
|
230
|
+
const column = options.columns().find((item) => item.id === id)
|
|
231
|
+
if (!column) return
|
|
232
|
+
state.widths[id] = clampWidth(column, width)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Drag & Drop (Reorder) ───────────────────────────────────────────
|
|
236
|
+
function startDrag(id: string, clientX: number, clientY: number): void {
|
|
237
|
+
state.dragging = { id, overId: null, x: clientX, y: clientY }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function dragOver(id: string, clientX: number, clientY: number): void {
|
|
241
|
+
if (!state.dragging) return
|
|
242
|
+
state.dragging.overId = id
|
|
243
|
+
state.dragging.x = clientX
|
|
244
|
+
state.dragging.y = clientY
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function endDrag(): void {
|
|
248
|
+
const active = state.dragging
|
|
249
|
+
state.dragging = null
|
|
250
|
+
if (active?.overId && active.overId !== active.id) moveColumn(active.id, active.overId, true)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Zurücksetzen / Zustand ──────────────────────────────────────────
|
|
254
|
+
function resetColumns(): void {
|
|
255
|
+
state.order = []
|
|
256
|
+
state.widths = {}
|
|
257
|
+
state.visibility = {}
|
|
258
|
+
state.pinning = {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Spalten-ids, die es nicht mehr gibt, aus dem Zustand entfernen. */
|
|
262
|
+
function pruneUnknown(): void {
|
|
263
|
+
const ids = new Set(options.columns().map((column) => column.id))
|
|
264
|
+
state.order = state.order.filter((id) => ids.has(id))
|
|
265
|
+
for (const map of [state.widths, state.visibility, state.pinning, state.measured] as Record<string, unknown>[]) {
|
|
266
|
+
for (const id of Object.keys(map)) if (!ids.has(id)) delete map[id]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
state,
|
|
272
|
+
isVisible,
|
|
273
|
+
pinOf,
|
|
274
|
+
widthOf,
|
|
275
|
+
setMeasured,
|
|
276
|
+
toggleColumn,
|
|
277
|
+
pinColumn,
|
|
278
|
+
getColumnPin,
|
|
279
|
+
currentOrder,
|
|
280
|
+
moveColumn,
|
|
281
|
+
shiftColumn,
|
|
282
|
+
startResize,
|
|
283
|
+
moveResize,
|
|
284
|
+
endResize,
|
|
285
|
+
setWidth,
|
|
286
|
+
startDrag,
|
|
287
|
+
dragOver,
|
|
288
|
+
endDrag,
|
|
289
|
+
resetColumns,
|
|
290
|
+
pruneUnknown,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type ColumnLayout<R extends Row = Row> = ReturnType<typeof useColumnLayout<R>>
|