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,496 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vi-browser">
|
|
3
|
+
<header class="vi-browser-head">
|
|
4
|
+
<wa-select
|
|
5
|
+
v-if="rootNodes.length > 1"
|
|
6
|
+
class="vi-browser-roots"
|
|
7
|
+
:value="String(activeRoot ?? '')"
|
|
8
|
+
size="small"
|
|
9
|
+
@change="onRootChange($event.target.value)"
|
|
10
|
+
>
|
|
11
|
+
<wa-option
|
|
12
|
+
v-for="root in rootNodes"
|
|
13
|
+
:key="root.key"
|
|
14
|
+
:value="String(root.key)"
|
|
15
|
+
>
|
|
16
|
+
{{ root.name }}
|
|
17
|
+
</wa-option>
|
|
18
|
+
</wa-select>
|
|
19
|
+
|
|
20
|
+
<nav
|
|
21
|
+
class="vi-browser-breadcrumb"
|
|
22
|
+
:aria-label="t('browser.path')"
|
|
23
|
+
>
|
|
24
|
+
<template
|
|
25
|
+
v-for="(entry, index) in breadcrumb"
|
|
26
|
+
:key="entry.key"
|
|
27
|
+
>
|
|
28
|
+
<span
|
|
29
|
+
v-if="index"
|
|
30
|
+
class="vi-browser-crumb-sep"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
/
|
|
34
|
+
</span>
|
|
35
|
+
<button
|
|
36
|
+
class="vi-browser-crumb"
|
|
37
|
+
type="button"
|
|
38
|
+
:aria-current="index === breadcrumb.length - 1 ? 'location' : undefined"
|
|
39
|
+
@click="navigateTo(entry.key)"
|
|
40
|
+
>
|
|
41
|
+
{{ entry.name }}
|
|
42
|
+
</button>
|
|
43
|
+
</template>
|
|
44
|
+
</nav>
|
|
45
|
+
|
|
46
|
+
<div class="vi-browser-actions">
|
|
47
|
+
<slot name="actions"></slot>
|
|
48
|
+
</div>
|
|
49
|
+
</header>
|
|
50
|
+
|
|
51
|
+
<wa-split-panel
|
|
52
|
+
class="vi-browser-body"
|
|
53
|
+
primary="start"
|
|
54
|
+
position-in-pixels="260"
|
|
55
|
+
snap="0px 260px"
|
|
56
|
+
>
|
|
57
|
+
<aside
|
|
58
|
+
slot="start"
|
|
59
|
+
class="vi-browser-tree"
|
|
60
|
+
>
|
|
61
|
+
<TreeNav
|
|
62
|
+
ref="treeEl"
|
|
63
|
+
:model-value="location"
|
|
64
|
+
:roots="treeRoots"
|
|
65
|
+
:load-children="loadNodeChildren"
|
|
66
|
+
container-id="tree"
|
|
67
|
+
:label="label"
|
|
68
|
+
@update:model-value="navigateTo($event)"
|
|
69
|
+
@children-loaded="onTreeChildren"
|
|
70
|
+
/>
|
|
71
|
+
</aside>
|
|
72
|
+
|
|
73
|
+
<main
|
|
74
|
+
slot="end"
|
|
75
|
+
ref="listEl"
|
|
76
|
+
class="vi-browser-list"
|
|
77
|
+
:class="{ 'is-drop-target': listIsDropTarget }"
|
|
78
|
+
@dragenter="onFileDragEnter"
|
|
79
|
+
@dragover="onFileDragOver"
|
|
80
|
+
@dragleave="onFileDragLeave"
|
|
81
|
+
@drop="onFileDrop"
|
|
82
|
+
>
|
|
83
|
+
<DataTable
|
|
84
|
+
ref="tableEl"
|
|
85
|
+
v-model:selected-keys="selectedKeys"
|
|
86
|
+
:data="visibleRows"
|
|
87
|
+
:columns="columns"
|
|
88
|
+
:row-key="rowKey"
|
|
89
|
+
:selectable="selectable"
|
|
90
|
+
:order-field="orderField"
|
|
91
|
+
:loading="loading"
|
|
92
|
+
drag-source="list"
|
|
93
|
+
:label="label"
|
|
94
|
+
:has-more="hasMore"
|
|
95
|
+
appearance="plain"
|
|
96
|
+
v-bind="tableProps"
|
|
97
|
+
@row-dblclick="onRowDoubleClick"
|
|
98
|
+
@row-reorder="$emit('reorder', $event)"
|
|
99
|
+
@load-more="$emit('load-more', $event)"
|
|
100
|
+
@page-change="$emit('page-change', $event)"
|
|
101
|
+
>
|
|
102
|
+
<template
|
|
103
|
+
v-for="(_, slotName) in $slots"
|
|
104
|
+
#[slotName]="scope"
|
|
105
|
+
>
|
|
106
|
+
<slot
|
|
107
|
+
:name="slotName"
|
|
108
|
+
v-bind="scope"
|
|
109
|
+
></slot>
|
|
110
|
+
</template>
|
|
111
|
+
</DataTable>
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
v-if="uploads && fileDragDepth > 0"
|
|
115
|
+
class="vi-browser-dropzone"
|
|
116
|
+
>
|
|
117
|
+
<wa-icon name="upload"></wa-icon>
|
|
118
|
+
<span>{{ uploadHint ?? t("browser.uploadHint") }}</span>
|
|
119
|
+
</div>
|
|
120
|
+
</main>
|
|
121
|
+
</wa-split-panel>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
124
|
+
|
|
125
|
+
<script setup>
|
|
126
|
+
import { useI18n } from "vue-i18n"
|
|
127
|
+
import { ref, computed, provide, watch } from "vue"
|
|
128
|
+
import { DataTable } from "../DataTable"
|
|
129
|
+
import TreeNav from "./TreeNav.vue"
|
|
130
|
+
import { createDragSession, dropModeFor, DRAG_SESSION } from "./dragSession"
|
|
131
|
+
|
|
132
|
+
// --------------------------------------------------------------------------- //
|
|
133
|
+
// Browser — Baum zur Navigation + DataTable als Liste //
|
|
134
|
+
// --------------------------------------------------------------------------- //
|
|
135
|
+
//
|
|
136
|
+
// GENERISCHER Kern: kennt kein ViUR. Die App (oder ViurBrowser.vue) liefert
|
|
137
|
+
// Rootnodes, den Inhalt des aktuellen Ortes (`rows`, Ordner zuerst) und einen
|
|
138
|
+
// Loader für die Baum-Ebenen — und bekommt Absichten als Events zurück:
|
|
139
|
+
// navigate — anderer Ort gewählt (Baum, Breadcrumb, Doppelklick auf Ordner)
|
|
140
|
+
// open — Doppelklick auf einen Eintrag (Leaf)
|
|
141
|
+
// move — Drag & Drop zwischen Liste und Baum bzw. in eine Ordnerzeile
|
|
142
|
+
// reorder — Umsortieren innerhalb der Liste (DataTable row-reorder, 1:1)
|
|
143
|
+
// upload — Dateien aus dem OS in die Liste gezogen
|
|
144
|
+
// Der Browser lädt selbst nichts und verschiebt nur die ANSICHT — Speichern
|
|
145
|
+
// und Nachladen entscheidet die App (siehe ViurBrowser für die ViUR-Variante).
|
|
146
|
+
|
|
147
|
+
const props = defineProps({
|
|
148
|
+
/** Wurzeln/Repositories: { key, name, icon? } */
|
|
149
|
+
rootNodes: { type: Array, default: () => [] },
|
|
150
|
+
/** Inhalt des aktuellen Ortes — Ordner zuerst (siehe rowKind). */
|
|
151
|
+
rows: { type: Array, default: () => [] },
|
|
152
|
+
columns: { type: Array, default: () => [] },
|
|
153
|
+
rowKey: { type: String, default: "key" },
|
|
154
|
+
/** Ordnet eine Zeile ein: "node" (Ordner) oder "leaf". */
|
|
155
|
+
rowKind: { type: Function, default: (row) => (row?.skelType === "node" ? "node" : "leaf") },
|
|
156
|
+
/** Kinder eines Baumknotens: (node) => Promise<BrowserNode[]> */
|
|
157
|
+
loadNodeChildren: { type: Function, default: null },
|
|
158
|
+
orderField: { type: String, default: "sortindex" },
|
|
159
|
+
selectable: { type: [String, Boolean], default: "multiple" },
|
|
160
|
+
loading: { type: Boolean, default: false },
|
|
161
|
+
/** OS-Dateien annehmen (nur Event `upload`, kein Upload-Code hier). */
|
|
162
|
+
uploads: { type: Boolean, default: false },
|
|
163
|
+
uploadHint: { type: [String, null], default: null },
|
|
164
|
+
label: { type: String, default: "Browser" },
|
|
165
|
+
/** Cursor-Nachladen: null = aus, sonst „gibt es mehr?" (DataTable-Leiste). */
|
|
166
|
+
hasMore: { type: [Boolean, null], default: null },
|
|
167
|
+
/** Weitere DataTable-Props (withSearch, size, …). */
|
|
168
|
+
tableProps: { type: Object, default: () => ({}) },
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const location = defineModel("location", { type: [String, Number, null], default: null })
|
|
172
|
+
const selectedKeys = defineModel("selectedKeys", { type: Array, default: () => [] })
|
|
173
|
+
const emit = defineEmits(["navigate", "open", "move", "reorder", "upload", "load-more", "page-change"])
|
|
174
|
+
|
|
175
|
+
const { t } = useI18n()
|
|
176
|
+
|
|
177
|
+
const treeEl = ref(null)
|
|
178
|
+
const tableEl = ref(null)
|
|
179
|
+
const listEl = ref(null)
|
|
180
|
+
|
|
181
|
+
// ── Rootnodes & Ort ───────────────────────────────────────────────────────────
|
|
182
|
+
// Der aktive Root ergibt sich aus dem Ort (Breadcrumb-Wurzel); ohne Ort gilt
|
|
183
|
+
// der erste Rootnode. Ein Wechsel im Select navigiert auf dessen Wurzel.
|
|
184
|
+
const nodeIndex = ref(new Map()) // key → { node, parent }
|
|
185
|
+
|
|
186
|
+
watch(
|
|
187
|
+
() => props.rootNodes,
|
|
188
|
+
(roots) => {
|
|
189
|
+
const next = new Map(nodeIndex.value)
|
|
190
|
+
for (const root of roots ?? []) next.set(String(root.key), { node: root, parent: null })
|
|
191
|
+
nodeIndex.value = next
|
|
192
|
+
if (location.value == null && roots?.length) navigateTo(roots[0].key)
|
|
193
|
+
},
|
|
194
|
+
{ immediate: true }
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const breadcrumb = computed(() => {
|
|
198
|
+
const out = []
|
|
199
|
+
let key = location.value
|
|
200
|
+
const seen = new Set()
|
|
201
|
+
while (key != null && !seen.has(String(key))) {
|
|
202
|
+
seen.add(String(key))
|
|
203
|
+
const entry = nodeIndex.value.get(String(key))
|
|
204
|
+
out.unshift({ key, name: entry?.node?.name ?? String(key) })
|
|
205
|
+
key = entry?.parent ?? null
|
|
206
|
+
}
|
|
207
|
+
return out
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const activeRoot = computed(() => breadcrumb.value[0]?.key ?? props.rootNodes[0]?.key ?? null)
|
|
211
|
+
const treeRoots = computed(() => {
|
|
212
|
+
if (props.rootNodes.length <= 1) return props.rootNodes
|
|
213
|
+
const active = props.rootNodes.find((root) => String(root.key) === String(activeRoot.value))
|
|
214
|
+
return active ? [active] : props.rootNodes
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
function rememberNodes(nodes, parent) {
|
|
218
|
+
const next = new Map(nodeIndex.value)
|
|
219
|
+
for (const node of nodes ?? []) next.set(String(node.key), { node, parent })
|
|
220
|
+
nodeIndex.value = next
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function onTreeChildren({ node, children }) {
|
|
224
|
+
rememberNodes(children, node.key)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function navigateTo(key) {
|
|
228
|
+
if (key === location.value) return
|
|
229
|
+
location.value = key
|
|
230
|
+
emit("navigate", { key, node: nodeIndex.value.get(String(key))?.node ?? null })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function onRootChange(value) {
|
|
234
|
+
const root = props.rootNodes.find((candidate) => String(candidate.key) === value)
|
|
235
|
+
if (root) navigateTo(root.key)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function onRowDoubleClick({ row, key }) {
|
|
239
|
+
if (props.rowKind(row) === "node") {
|
|
240
|
+
// Ordner aus der Liste merken, damit der Breadcrumb seinen Namen kennt.
|
|
241
|
+
rememberNodes([{ key, name: rowName(row), data: row }], location.value)
|
|
242
|
+
navigateTo(key)
|
|
243
|
+
} else {
|
|
244
|
+
emit("open", { row, key })
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function rowName(row) {
|
|
249
|
+
const value = row?.name
|
|
250
|
+
return typeof value === "string" ? value : (value?.name ?? String(row?.[props.rowKey] ?? ""))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Lokale Sicht auf die Liste ────────────────────────────────────────────────
|
|
254
|
+
// Ein Zug ist rein clientseitig: Zeilen, die den aktuellen Ort VERLASSEN,
|
|
255
|
+
// werden ausgeblendet; Knoten, die aus dem Baum HIERHER gezogen wurden, werden
|
|
256
|
+
// eingefügt. `rows` (die Daten der App) bleiben unangetastet — neue Daten oder
|
|
257
|
+
// ein Ortswechsel verwerfen die Überlagerung.
|
|
258
|
+
const movedAway = ref(new Set())
|
|
259
|
+
const movedIn = ref([])
|
|
260
|
+
|
|
261
|
+
const visibleRows = computed(() => {
|
|
262
|
+
let out = props.rows
|
|
263
|
+
if (movedAway.value.size) {
|
|
264
|
+
out = out.filter((row) => !movedAway.value.has(String(row[props.rowKey])))
|
|
265
|
+
}
|
|
266
|
+
if (movedIn.value.length) {
|
|
267
|
+
const present = new Set(out.map((row) => String(row[props.rowKey])))
|
|
268
|
+
out = [...out, ...movedIn.value.filter((row) => !present.has(String(row[props.rowKey])))]
|
|
269
|
+
}
|
|
270
|
+
return out
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
function resetLocalMoves() {
|
|
274
|
+
if (movedAway.value.size) movedAway.value = new Set()
|
|
275
|
+
if (movedIn.value.length) movedIn.value = []
|
|
276
|
+
}
|
|
277
|
+
watch(() => props.rows, resetLocalMoves)
|
|
278
|
+
watch(location, resetLocalMoves)
|
|
279
|
+
|
|
280
|
+
// ── Drag-Session (Liste ↔ Baum) ───────────────────────────────────────────────
|
|
281
|
+
const session = createDragSession((payload, target) => {
|
|
282
|
+
// In sich selbst oder den eigenen Teilbaum fällt nichts: Ziel verwerfen.
|
|
283
|
+
if (payload.keys.some((key) => String(key) === String(target.key))) return
|
|
284
|
+
let parent = target.key != null ? nodeIndex.value.get(String(target.key))?.parent : null
|
|
285
|
+
const seen = new Set()
|
|
286
|
+
while (parent != null && !seen.has(String(parent))) {
|
|
287
|
+
if (payload.keys.some((key) => String(key) === String(parent))) return
|
|
288
|
+
seen.add(String(parent))
|
|
289
|
+
parent = nodeIndex.value.get(String(parent))?.parent ?? null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Ansicht sofort nachziehen — ohne Reload.
|
|
293
|
+
const targetIsCurrent = location.value != null && String(target.key) === String(location.value)
|
|
294
|
+
if (targetIsCurrent) {
|
|
295
|
+
const away = new Set(movedAway.value)
|
|
296
|
+
for (const key of payload.keys) away.delete(String(key))
|
|
297
|
+
movedAway.value = away
|
|
298
|
+
if (payload.source === "tree") {
|
|
299
|
+
// Baumknoten in die offene Ebene gezogen: als Ordnerzeile einfügen.
|
|
300
|
+
const rows = payload.rows.map((row) => row?.data ?? row).filter(Boolean)
|
|
301
|
+
movedIn.value = [...movedIn.value, ...rows]
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
movedAway.value = new Set([...movedAway.value, ...payload.keys.map((key) => String(key))])
|
|
305
|
+
movedIn.value = movedIn.value.filter(
|
|
306
|
+
(row) => !payload.keys.some((key) => String(key) === String(row[props.rowKey]))
|
|
307
|
+
)
|
|
308
|
+
// Was den Ort verlassen hat, ist auch nicht mehr ausgewählt.
|
|
309
|
+
selectedKeys.value = selectedKeys.value.filter(
|
|
310
|
+
(selected) => !payload.keys.some((key) => String(key) === String(selected))
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Auch der BAUM zieht lokal nach: verschobene ORDNER wechseln (oder
|
|
315
|
+
// verlassen) ihre geladene Ebene — ohne Reload. Blätter tauchen im
|
|
316
|
+
// Navigations-Baum nicht auf und brauchen nichts.
|
|
317
|
+
for (const row of payload.rows) {
|
|
318
|
+
const raw = row?.data ?? row
|
|
319
|
+
const isNode = payload.source === "tree" || props.rowKind(raw) === "node"
|
|
320
|
+
if (!isNode) continue
|
|
321
|
+
const key = raw?.[props.rowKey] ?? row?.key
|
|
322
|
+
const node = { key, name: nodeIndex.value.get(String(key))?.node?.name ?? rowName(raw), data: raw }
|
|
323
|
+
treeEl.value?.moveNode?.(node, target.key)
|
|
324
|
+
rememberNodes([node], target.key) // Breadcrumb-Elternkette aktuell halten
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
emit("move", {
|
|
328
|
+
keys: payload.keys,
|
|
329
|
+
rows: payload.rows,
|
|
330
|
+
target: target.key,
|
|
331
|
+
mode: target.mode,
|
|
332
|
+
source: payload.source === "tree" ? "tree" : "list",
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
provide(DRAG_SESSION, session)
|
|
336
|
+
|
|
337
|
+
// Die Liste ist selbst Drop-Ziel — in beide Richtungen:
|
|
338
|
+
// • Die MITTE einer Ordnerzeile heißt „hinein"; die Ränder (davor/dahinter)
|
|
339
|
+
// überlässt sie der internen Umsortierung der Tabelle.
|
|
340
|
+
// • Kommt der Zug aus dem BAUM, ist auch der Rest der Liste (Leaf-Zeilen,
|
|
341
|
+
// freie Fläche) ein Ziel: „in den gerade geöffneten Ordner".
|
|
342
|
+
session.register({
|
|
343
|
+
id: "list",
|
|
344
|
+
el: () => listEl.value,
|
|
345
|
+
targetFor: (hit, _x, y) => {
|
|
346
|
+
const fromTree = session.state.payload?.source === "tree"
|
|
347
|
+
const intoCurrent =
|
|
348
|
+
fromTree && location.value != null ? { container: "list", key: location.value, mode: "inside" } : null
|
|
349
|
+
const rowEl = hit.closest?.("[data-key]")
|
|
350
|
+
if (!rowEl) return intoCurrent
|
|
351
|
+
const row = visibleRows.value.find((candidate) => String(candidate[props.rowKey]) === rowEl.dataset.key)
|
|
352
|
+
if (!row || props.rowKind(row) !== "node") return intoCurrent
|
|
353
|
+
if (dropModeFor(rowEl.getBoundingClientRect(), y) !== "inside") return intoCurrent
|
|
354
|
+
return { container: "list", key: row[props.rowKey], mode: "inside" }
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
/** Zug aus dem Baum zielt auf die offene Ebene → die ganze Liste markieren. */
|
|
359
|
+
const listIsDropTarget = computed(
|
|
360
|
+
() =>
|
|
361
|
+
session.state.over?.container === "list" &&
|
|
362
|
+
location.value != null &&
|
|
363
|
+
String(session.state.over?.key) === String(location.value)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
// ── Upload-Dropzone (native, nur für OS-Dateien) ─────────────────────────────
|
|
367
|
+
const fileDragDepth = ref(0)
|
|
368
|
+
const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files")
|
|
369
|
+
function onFileDragEnter(event) {
|
|
370
|
+
if (!props.uploads || !hasFiles(event)) return
|
|
371
|
+
event.preventDefault()
|
|
372
|
+
fileDragDepth.value++
|
|
373
|
+
}
|
|
374
|
+
function onFileDragOver(event) {
|
|
375
|
+
if (!props.uploads || !hasFiles(event)) return
|
|
376
|
+
event.preventDefault()
|
|
377
|
+
}
|
|
378
|
+
function onFileDragLeave(event) {
|
|
379
|
+
if (!props.uploads || !hasFiles(event)) return
|
|
380
|
+
fileDragDepth.value = Math.max(0, fileDragDepth.value - 1)
|
|
381
|
+
}
|
|
382
|
+
function onFileDrop(event) {
|
|
383
|
+
if (!props.uploads || !hasFiles(event)) return
|
|
384
|
+
event.preventDefault()
|
|
385
|
+
fileDragDepth.value = 0
|
|
386
|
+
const files = Array.from(event.dataTransfer?.files ?? [])
|
|
387
|
+
if (files.length) emit("upload", { files, target: location.value })
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
|
|
391
|
+
</script>
|
|
392
|
+
|
|
393
|
+
<style scoped>
|
|
394
|
+
.vi-browser {
|
|
395
|
+
display: flex;
|
|
396
|
+
flex-direction: column;
|
|
397
|
+
min-block-size: 0;
|
|
398
|
+
block-size: 100%;
|
|
399
|
+
/* Gerahmt wie das Listen-Widget (DataTable `appearance="outlined"`): EIN
|
|
400
|
+
Kasten um Baum und Liste. Die Tabelle darin bleibt deshalb rahmenlos
|
|
401
|
+
(appearance="plain" unten), sonst stünden zwei Rahmen ineinander. */
|
|
402
|
+
background-color: var(--wa-color-surface-default, #fff);
|
|
403
|
+
border: var(--wa-border-width-s, 1px) solid var(--wa-color-surface-border, #ddd);
|
|
404
|
+
border-radius: var(--wa-border-radius-m, 0.5rem);
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
/* Eine Textgröße für den ganzen Kasten — dieselbe wie in der Tabelle
|
|
407
|
+
(.vi-dt), damit Breadcrumb und Baum nicht größer wirken als die Liste. */
|
|
408
|
+
font-size: var(--wa-font-size-s, 0.875rem);
|
|
409
|
+
}
|
|
410
|
+
.vi-browser-head {
|
|
411
|
+
display: flex;
|
|
412
|
+
align-items: center;
|
|
413
|
+
gap: var(--wa-space-s, 0.5rem);
|
|
414
|
+
padding: var(--wa-space-2xs, 0.25rem) var(--wa-space-xs, 0.5rem);
|
|
415
|
+
border-block-end: 1px solid var(--wa-color-surface-border, #ddd);
|
|
416
|
+
/* Abgesetzt wie der Kopf der Tabelle (--dt-header-background). */
|
|
417
|
+
background-color: var(--wa-color-surface-lowered, #f4f4f5);
|
|
418
|
+
}
|
|
419
|
+
.vi-browser-roots {
|
|
420
|
+
min-inline-size: 10rem;
|
|
421
|
+
}
|
|
422
|
+
.vi-browser-breadcrumb {
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: var(--wa-space-2xs, 0.25rem);
|
|
426
|
+
overflow: hidden;
|
|
427
|
+
flex: 1;
|
|
428
|
+
}
|
|
429
|
+
.vi-browser-crumb {
|
|
430
|
+
border: none;
|
|
431
|
+
background: none;
|
|
432
|
+
padding: var(--wa-space-2xs, 0.25rem);
|
|
433
|
+
border-radius: var(--wa-border-radius-s, 0.25rem);
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
color: inherit;
|
|
436
|
+
white-space: nowrap;
|
|
437
|
+
}
|
|
438
|
+
.vi-browser-crumb:hover {
|
|
439
|
+
background: var(--wa-color-neutral-fill-quiet, rgba(0 0 0 / 0.05));
|
|
440
|
+
}
|
|
441
|
+
.vi-browser-crumb[aria-current="location"] {
|
|
442
|
+
font-weight: 600;
|
|
443
|
+
}
|
|
444
|
+
.vi-browser-crumb-sep {
|
|
445
|
+
color: var(--wa-color-text-quiet, #888);
|
|
446
|
+
}
|
|
447
|
+
.vi-browser-body {
|
|
448
|
+
flex: 1;
|
|
449
|
+
min-block-size: 0;
|
|
450
|
+
/* <wa-split-panel> IST das Grid (display: grid am Host). Seine Zeile ist von
|
|
451
|
+
sich aus inhaltshoch und läuft damit über die feste Höhe hinaus — die
|
|
452
|
+
Tabelle wäre dann so hoch wie ihre Zeilen und würde die Seite verlängern
|
|
453
|
+
statt in sich zu scrollen. Eine Zeile über die volle Höhe bindet die
|
|
454
|
+
Panels an die Fläche. Gilt für die waagerechte Aufteilung, die hier fest
|
|
455
|
+
eingestellt ist. */
|
|
456
|
+
grid-template-rows: 100%;
|
|
457
|
+
/* Seitenleiste nicht kleiner als 10rem, Liste behält mindestens die Hälfte */
|
|
458
|
+
--min: 10rem;
|
|
459
|
+
--max: 50%;
|
|
460
|
+
}
|
|
461
|
+
.vi-browser-tree {
|
|
462
|
+
block-size: 100%;
|
|
463
|
+
overflow: auto;
|
|
464
|
+
padding: var(--wa-space-2xs, 0.25rem);
|
|
465
|
+
}
|
|
466
|
+
.vi-browser-list {
|
|
467
|
+
block-size: 100%;
|
|
468
|
+
position: relative;
|
|
469
|
+
min-inline-size: 0;
|
|
470
|
+
min-block-size: 0;
|
|
471
|
+
display: flex;
|
|
472
|
+
flex-direction: column;
|
|
473
|
+
}
|
|
474
|
+
.vi-browser-list > :deep(.vi-dt) {
|
|
475
|
+
flex: 1;
|
|
476
|
+
min-block-size: 0;
|
|
477
|
+
}
|
|
478
|
+
.vi-browser-list.is-drop-target {
|
|
479
|
+
outline: 2px dashed var(--wa-color-brand-50, #2563eb);
|
|
480
|
+
outline-offset: -2px;
|
|
481
|
+
}
|
|
482
|
+
.vi-browser-dropzone {
|
|
483
|
+
position: absolute;
|
|
484
|
+
inset: 0;
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
align-items: center;
|
|
488
|
+
justify-content: center;
|
|
489
|
+
gap: var(--wa-space-s, 0.5rem);
|
|
490
|
+
background: color-mix(in oklab, var(--wa-color-surface-default, #fff) 80%, transparent);
|
|
491
|
+
border: 2px dashed var(--wa-color-brand-50, #2563eb);
|
|
492
|
+
border-radius: var(--wa-border-radius-m, 0.5rem);
|
|
493
|
+
pointer-events: none;
|
|
494
|
+
font-weight: 600;
|
|
495
|
+
}
|
|
496
|
+
</style>
|