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,408 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="rootEl"
|
|
4
|
+
class="vi-treenav"
|
|
5
|
+
role="tree"
|
|
6
|
+
:aria-label="label ?? t('browser.navigation')"
|
|
7
|
+
@keydown="onKeydown"
|
|
8
|
+
>
|
|
9
|
+
<template
|
|
10
|
+
v-for="entry in visibleEntries"
|
|
11
|
+
:key="entry.node.key"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="vi-treenav-row"
|
|
15
|
+
:class="{
|
|
16
|
+
'is-current': entry.node.key === location,
|
|
17
|
+
'is-drop': session?.state.over?.container === containerId && session?.state.over?.key === entry.node.key,
|
|
18
|
+
}"
|
|
19
|
+
role="treeitem"
|
|
20
|
+
:aria-level="entry.depth + 1"
|
|
21
|
+
:aria-expanded="canExpand(entry.node) ? expanded.has(entry.node.key) : undefined"
|
|
22
|
+
:aria-selected="entry.node.key === location"
|
|
23
|
+
:tabindex="entry.node.key === activeKey ? 0 : -1"
|
|
24
|
+
:style="{ paddingInlineStart: `calc(${entry.depth} * var(--wa-space-m, 1rem))` }"
|
|
25
|
+
:data-tree-key="entry.node.key"
|
|
26
|
+
@click="open(entry.node)"
|
|
27
|
+
@focus="activeKey = entry.node.key"
|
|
28
|
+
@pointerdown="onPointerDown(entry.node, $event)"
|
|
29
|
+
>
|
|
30
|
+
<span
|
|
31
|
+
class="vi-treenav-arrow"
|
|
32
|
+
:class="{ 'is-open': expanded.has(entry.node.key), 'is-none': !canExpand(entry.node) }"
|
|
33
|
+
@click.stop="toggle(entry.node)"
|
|
34
|
+
>
|
|
35
|
+
<wa-spinner v-if="loading.has(entry.node.key)"></wa-spinner>
|
|
36
|
+
<wa-icon
|
|
37
|
+
v-else
|
|
38
|
+
name="chevron-right"
|
|
39
|
+
></wa-icon>
|
|
40
|
+
</span>
|
|
41
|
+
<wa-icon
|
|
42
|
+
class="vi-treenav-icon"
|
|
43
|
+
:name="entry.node.icon || 'folder'"
|
|
44
|
+
></wa-icon>
|
|
45
|
+
<span class="vi-treenav-name">{{ entry.node.name }}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup>
|
|
52
|
+
import { useI18n } from "vue-i18n"
|
|
53
|
+
import { ref, computed, watch, inject, onMounted, onBeforeUnmount } from "vue"
|
|
54
|
+
import { DRAG_SESSION } from "./dragSession"
|
|
55
|
+
|
|
56
|
+
// Schlanker NAVIGATIONS-Baum: nur Knoten (Name, Icon, Pfeil), Kinder werden
|
|
57
|
+
// über `loadChildren` nachgeladen. Bewusst keine DataTable — Navigation ist
|
|
58
|
+
// kein Datengrid. Als Drop-Ziel registriert er sich an der Drag-Session des
|
|
59
|
+
// Browsers (immer "inside": in den getroffenen Ordner).
|
|
60
|
+
|
|
61
|
+
const props = defineProps({
|
|
62
|
+
/** Wurzeln (z. B. die Rootnodes) — { key, name, icon?, hasChildren? } */
|
|
63
|
+
roots: { type: Array, default: () => [] },
|
|
64
|
+
/** Kinder eines Knotens holen: (node) => Promise<BrowserNode[]> */
|
|
65
|
+
loadChildren: { type: Function, default: null },
|
|
66
|
+
label: { type: [String, null], default: null },
|
|
67
|
+
/** Container-Id in der Drag-Session */
|
|
68
|
+
containerId: { type: String, default: "tree" },
|
|
69
|
+
})
|
|
70
|
+
const location = defineModel({ type: [String, Number, null], default: null })
|
|
71
|
+
const emit = defineEmits(["navigate", "children-loaded"])
|
|
72
|
+
|
|
73
|
+
const { t } = useI18n()
|
|
74
|
+
|
|
75
|
+
const rootEl = ref(null)
|
|
76
|
+
const expanded = ref(new Set())
|
|
77
|
+
const loading = ref(new Set())
|
|
78
|
+
const children = ref(new Map()) // String(key) → BrowserNode[]
|
|
79
|
+
// Lokal eingefügte Knoten für noch nicht geladene Ebenen: werden beim ersten
|
|
80
|
+
// Laden der Ebene hineingemerged (der Server kann kurz hinterherhinken).
|
|
81
|
+
const pendingInserts = ref(new Map()) // String(key) → BrowserNode[]
|
|
82
|
+
const bucketOf = (key) => children.value.get(String(key))
|
|
83
|
+
const activeKey = ref(null)
|
|
84
|
+
|
|
85
|
+
const session = inject(DRAG_SESSION, null)
|
|
86
|
+
|
|
87
|
+
const visibleEntries = computed(() => {
|
|
88
|
+
const out = []
|
|
89
|
+
const walk = (nodes, depth) => {
|
|
90
|
+
for (const node of nodes ?? []) {
|
|
91
|
+
out.push({ node, depth })
|
|
92
|
+
if (expanded.value.has(node.key)) walk(bucketOf(node.key), depth + 1)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
walk(props.roots, 0)
|
|
96
|
+
return out
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
watch(
|
|
100
|
+
() => props.roots,
|
|
101
|
+
(roots) => {
|
|
102
|
+
if (activeKey.value === null && roots?.length) activeKey.value = roots[0].key
|
|
103
|
+
void expandRoots()
|
|
104
|
+
},
|
|
105
|
+
{ immediate: true }
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
/** Die Wurzeln (Rootnodes) sind immer ausgeklappt — ohne ersten Klick. */
|
|
109
|
+
async function expandRoots() {
|
|
110
|
+
for (const root of props.roots ?? []) {
|
|
111
|
+
if (expanded.value.has(root.key) || root.hasChildren === false) continue
|
|
112
|
+
await fetchChildren(root).catch(() => null)
|
|
113
|
+
if ((bucketOf(root.key) ?? []).length) {
|
|
114
|
+
expanded.value = new Set(expanded.value).add(root.key)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function canExpand(node) {
|
|
120
|
+
if (node.hasChildren === false) return false
|
|
121
|
+
const known = bucketOf(node.key)
|
|
122
|
+
return known ? known.length > 0 : true
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function fetchChildren(node, force = false) {
|
|
126
|
+
if (!props.loadChildren || loading.value.has(node.key)) return
|
|
127
|
+
if (!force && children.value.has(String(node.key))) return
|
|
128
|
+
loading.value = new Set(loading.value).add(node.key)
|
|
129
|
+
try {
|
|
130
|
+
const result = (await props.loadChildren(node)) ?? []
|
|
131
|
+
// Lokal (per Drag & Drop) hierher verschobene Knoten hineinmergen — der
|
|
132
|
+
// Server kann den Zug noch nicht kennen. Was er schon liefert, ist damit
|
|
133
|
+
// angekommen und braucht keinen Merker mehr.
|
|
134
|
+
const bucketKey = String(node.key)
|
|
135
|
+
const pending = pendingInserts.value.get(bucketKey) ?? []
|
|
136
|
+
const merged = [...result]
|
|
137
|
+
for (const entry of pending) {
|
|
138
|
+
if (!merged.some((candidate) => String(candidate.key) === String(entry.key))) merged.push(entry)
|
|
139
|
+
}
|
|
140
|
+
const remaining = pending.filter(
|
|
141
|
+
(entry) => !result.some((candidate) => String(candidate.key) === String(entry.key))
|
|
142
|
+
)
|
|
143
|
+
if (remaining.length !== pending.length || !remaining.length) {
|
|
144
|
+
const nextPending = new Map(pendingInserts.value)
|
|
145
|
+
if (remaining.length) nextPending.set(bucketKey, remaining)
|
|
146
|
+
else nextPending.delete(bucketKey)
|
|
147
|
+
pendingInserts.value = nextPending
|
|
148
|
+
}
|
|
149
|
+
const next = new Map(children.value)
|
|
150
|
+
next.set(bucketKey, merged)
|
|
151
|
+
children.value = next
|
|
152
|
+
emit("children-loaded", { node, children: merged })
|
|
153
|
+
} finally {
|
|
154
|
+
const next = new Set(loading.value)
|
|
155
|
+
next.delete(node.key)
|
|
156
|
+
loading.value = next
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function toggle(node) {
|
|
161
|
+
if (!canExpand(node)) return
|
|
162
|
+
const next = new Set(expanded.value)
|
|
163
|
+
if (next.has(node.key)) {
|
|
164
|
+
next.delete(node.key)
|
|
165
|
+
expanded.value = next
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await fetchChildren(node)
|
|
170
|
+
} catch {
|
|
171
|
+
return // Ebene nicht ladbar → zubleiben
|
|
172
|
+
}
|
|
173
|
+
if ((bucketOf(node.key) ?? []).length) {
|
|
174
|
+
next.add(node.key)
|
|
175
|
+
expanded.value = next
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function open(node) {
|
|
180
|
+
activeKey.value = node.key
|
|
181
|
+
location.value = node.key
|
|
182
|
+
emit("navigate", node)
|
|
183
|
+
void toggle(node) // aufklappen gehört zum Navigieren dazu; zuklappen macht nur der Pfeil
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Knoten LOKAL aus seiner bisherigen Ebene nehmen und — wenn die Ziel-Ebene
|
|
188
|
+
* schon geladen ist — dort einhängen. Kein Request: eine noch nicht geladene
|
|
189
|
+
* Ziel-Ebene holt ihn beim ersten Aufklappen ohnehin frisch vom Server.
|
|
190
|
+
* `node` darf ein voller BrowserNode sein (für Knoten, die der Baum selbst
|
|
191
|
+
* noch nicht kennt, z. B. Ordnerzeilen aus der Liste) oder nur ein Schlüssel.
|
|
192
|
+
*/
|
|
193
|
+
function moveNode(node, targetKey) {
|
|
194
|
+
const key = typeof node === "object" && node !== null ? node.key : node
|
|
195
|
+
const removed = removeNode(key)
|
|
196
|
+
const entry = removed ?? (typeof node === "object" && node !== null ? node : null)
|
|
197
|
+
if (entry == null || targetKey == null) return
|
|
198
|
+
const bucketKey = String(targetKey)
|
|
199
|
+
if (children.value.has(bucketKey)) {
|
|
200
|
+
const next = new Map(children.value)
|
|
201
|
+
next.set(bucketKey, [...(next.get(bucketKey) ?? []), entry])
|
|
202
|
+
children.value = next
|
|
203
|
+
} else {
|
|
204
|
+
// Ziel-Ebene noch nie geladen: vormerken — beim ersten Laden wird der
|
|
205
|
+
// Knoten hineingemerged (siehe fetchChildren). Kein Request nötig.
|
|
206
|
+
const next = new Map(pendingInserts.value)
|
|
207
|
+
next.set(bucketKey, [...(next.get(bucketKey) ?? []), entry])
|
|
208
|
+
pendingInserts.value = next
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Knoten lokal aus allen geladenen Ebenen entfernen; liefert ihn zurück. */
|
|
213
|
+
function removeNode(key) {
|
|
214
|
+
let removed = null
|
|
215
|
+
const next = new Map(children.value)
|
|
216
|
+
for (const [parent, list] of next) {
|
|
217
|
+
const index = list.findIndex((candidate) => String(candidate.key) === String(key))
|
|
218
|
+
if (index === -1) continue
|
|
219
|
+
removed = list[index]
|
|
220
|
+
next.set(parent, [...list.slice(0, index), ...list.slice(index + 1)])
|
|
221
|
+
}
|
|
222
|
+
if (removed) children.value = next
|
|
223
|
+
for (const [parent, list] of pendingInserts.value) {
|
|
224
|
+
const index = list.findIndex((candidate) => String(candidate.key) === String(key))
|
|
225
|
+
if (index === -1) continue
|
|
226
|
+
removed = removed ?? list[index]
|
|
227
|
+
const nextPending = new Map(pendingInserts.value)
|
|
228
|
+
const shortened = [...list.slice(0, index), ...list.slice(index + 1)]
|
|
229
|
+
if (shortened.length) nextPending.set(parent, shortened)
|
|
230
|
+
else nextPending.delete(parent)
|
|
231
|
+
pendingInserts.value = nextPending
|
|
232
|
+
}
|
|
233
|
+
return removed
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Ebene eines Knotens verwerfen und neu holen (z. B. nach einem Move). */
|
|
237
|
+
function reloadChildren(key) {
|
|
238
|
+
const entry = visibleEntries.value.find((candidate) => String(candidate.node.key) === String(key))
|
|
239
|
+
if (!entry) return Promise.resolve()
|
|
240
|
+
return fetchChildren(entry.node, true).catch(() => null)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function reset() {
|
|
244
|
+
expanded.value = new Set()
|
|
245
|
+
children.value = new Map()
|
|
246
|
+
pendingInserts.value = new Map()
|
|
247
|
+
void expandRoots()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Tastatur (WAI-ARIA tree) ──────────────────────────────────────────────────
|
|
251
|
+
function onKeydown(event) {
|
|
252
|
+
const entries = visibleEntries.value
|
|
253
|
+
const index = entries.findIndex((entry) => entry.node.key === activeKey.value)
|
|
254
|
+
if (index === -1) return
|
|
255
|
+
const focusAt = (i) => {
|
|
256
|
+
const entry = entries[Math.max(0, Math.min(entries.length - 1, i))]
|
|
257
|
+
activeKey.value = entry.node.key
|
|
258
|
+
rootEl.value?.querySelector(`[data-tree-key="${CSS.escape(String(entry.node.key))}"]`)?.focus()
|
|
259
|
+
}
|
|
260
|
+
const entry = entries[index]
|
|
261
|
+
switch (event.key) {
|
|
262
|
+
case "ArrowDown":
|
|
263
|
+
focusAt(index + 1)
|
|
264
|
+
break
|
|
265
|
+
case "ArrowUp":
|
|
266
|
+
focusAt(index - 1)
|
|
267
|
+
break
|
|
268
|
+
case "ArrowRight":
|
|
269
|
+
if (!expanded.value.has(entry.node.key)) void toggle(entry.node)
|
|
270
|
+
else focusAt(index + 1)
|
|
271
|
+
break
|
|
272
|
+
case "ArrowLeft":
|
|
273
|
+
if (expanded.value.has(entry.node.key)) void toggle(entry.node)
|
|
274
|
+
else {
|
|
275
|
+
const parentIndex = entries.slice(0, index).findLastIndex((candidate) => candidate.depth === entry.depth - 1)
|
|
276
|
+
if (parentIndex !== -1) focusAt(parentIndex)
|
|
277
|
+
}
|
|
278
|
+
break
|
|
279
|
+
case "Home":
|
|
280
|
+
focusAt(0)
|
|
281
|
+
break
|
|
282
|
+
case "End":
|
|
283
|
+
focusAt(entries.length - 1)
|
|
284
|
+
break
|
|
285
|
+
case "Enter":
|
|
286
|
+
case " ":
|
|
287
|
+
open(entry.node)
|
|
288
|
+
break
|
|
289
|
+
default:
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
event.preventDefault()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Drag-Quelle: Knoten in die Liste (oder andere Knoten) ziehen ─────────────
|
|
296
|
+
// Erst ab einer kleinen Bewegungsschwelle wird aus dem Klick ein Zug — Klicks
|
|
297
|
+
// zum Navigieren bleiben Klicks.
|
|
298
|
+
function onPointerDown(node, event) {
|
|
299
|
+
if (!session || event.button !== 0) return
|
|
300
|
+
// Wurzeln (Rootnodes) sind nicht verschiebbar.
|
|
301
|
+
if (props.roots.some((root) => String(root.key) === String(node.key))) return
|
|
302
|
+
const startX = event.clientX
|
|
303
|
+
const startY = event.clientY
|
|
304
|
+
let dragging = false
|
|
305
|
+
const move = (moveEvent) => {
|
|
306
|
+
if (!dragging && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 5) return
|
|
307
|
+
if (!dragging) {
|
|
308
|
+
dragging = true
|
|
309
|
+
session.start({ keys: [node.key], rows: [node.data ?? node], source: props.containerId })
|
|
310
|
+
}
|
|
311
|
+
session.update(moveEvent.clientX, moveEvent.clientY)
|
|
312
|
+
}
|
|
313
|
+
const up = (upEvent) => {
|
|
314
|
+
window.removeEventListener("pointermove", move)
|
|
315
|
+
window.removeEventListener("pointerup", up)
|
|
316
|
+
if (!dragging) return
|
|
317
|
+
session.update(upEvent.clientX, upEvent.clientY)
|
|
318
|
+
if (session.state.over) session.drop()
|
|
319
|
+
else session.cancel()
|
|
320
|
+
}
|
|
321
|
+
window.addEventListener("pointermove", move)
|
|
322
|
+
window.addEventListener("pointerup", up)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Drop-Ziel der Drag-Session ────────────────────────────────────────────────
|
|
326
|
+
let unregister = null
|
|
327
|
+
let hoverTimer = null
|
|
328
|
+
let hoverKey = null
|
|
329
|
+
|
|
330
|
+
onMounted(() => {
|
|
331
|
+
if (!session) return
|
|
332
|
+
unregister = session.register({
|
|
333
|
+
id: props.containerId,
|
|
334
|
+
el: () => rootEl.value,
|
|
335
|
+
targetFor: (hit) => {
|
|
336
|
+
const row = hit.closest?.("[data-tree-key]")
|
|
337
|
+
if (!row) return null
|
|
338
|
+
const key = row.dataset.treeKey
|
|
339
|
+
// Über einem zugeklappten Ordner verweilen klappt ihn auf.
|
|
340
|
+
if (hoverKey !== key) {
|
|
341
|
+
hoverKey = key
|
|
342
|
+
clearTimeout(hoverTimer)
|
|
343
|
+
hoverTimer = setTimeout(() => {
|
|
344
|
+
const entry = visibleEntries.value.find((candidate) => String(candidate.node.key) === key)
|
|
345
|
+
if (entry && !expanded.value.has(entry.node.key)) void toggle(entry.node)
|
|
346
|
+
}, 700)
|
|
347
|
+
}
|
|
348
|
+
return { container: props.containerId, key, mode: "inside" }
|
|
349
|
+
},
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
onBeforeUnmount(() => {
|
|
353
|
+
unregister?.()
|
|
354
|
+
clearTimeout(hoverTimer)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
defineExpose({ reloadChildren, reset, moveNode, removeNode, expanded, children })
|
|
358
|
+
</script>
|
|
359
|
+
|
|
360
|
+
<style scoped>
|
|
361
|
+
.vi-treenav {
|
|
362
|
+
min-inline-size: 0;
|
|
363
|
+
overflow: auto;
|
|
364
|
+
padding-block: var(--wa-space-2xs, 0.25rem);
|
|
365
|
+
}
|
|
366
|
+
.vi-treenav-row {
|
|
367
|
+
display: flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
gap: var(--wa-space-2xs, 0.25rem);
|
|
370
|
+
padding-block: var(--wa-space-2xs, 0.25rem);
|
|
371
|
+
padding-inline-end: var(--wa-space-s, 0.5rem);
|
|
372
|
+
border-radius: var(--wa-border-radius-s, 0.25rem);
|
|
373
|
+
cursor: pointer;
|
|
374
|
+
user-select: none;
|
|
375
|
+
white-space: nowrap;
|
|
376
|
+
}
|
|
377
|
+
.vi-treenav-row:hover {
|
|
378
|
+
background: var(--wa-color-neutral-fill-quiet, rgba(0 0 0 / 0.05));
|
|
379
|
+
}
|
|
380
|
+
.vi-treenav-row.is-current {
|
|
381
|
+
background: var(--wa-color-brand-fill-quiet, rgba(0 80 255 / 0.1));
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
}
|
|
384
|
+
.vi-treenav-row.is-drop {
|
|
385
|
+
outline: 2px dashed var(--wa-color-brand-50, #2563eb);
|
|
386
|
+
outline-offset: -2px;
|
|
387
|
+
}
|
|
388
|
+
.vi-treenav-arrow {
|
|
389
|
+
display: inline-flex;
|
|
390
|
+
inline-size: 1.25em;
|
|
391
|
+
justify-content: center;
|
|
392
|
+
transition: rotate 0.15s ease;
|
|
393
|
+
flex: none;
|
|
394
|
+
}
|
|
395
|
+
.vi-treenav-arrow.is-open {
|
|
396
|
+
rotate: 90deg;
|
|
397
|
+
}
|
|
398
|
+
.vi-treenav-arrow.is-none {
|
|
399
|
+
visibility: hidden;
|
|
400
|
+
}
|
|
401
|
+
.vi-treenav-icon {
|
|
402
|
+
flex: none;
|
|
403
|
+
}
|
|
404
|
+
.vi-treenav-name {
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
text-overflow: ellipsis;
|
|
407
|
+
}
|
|
408
|
+
</style>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DefineComponent } from "vue"
|
|
2
|
+
import type { RowKey } from "../DataTable/types"
|
|
3
|
+
import type { BrowserMovePayload, BrowserUploadPayload } from "./types"
|
|
4
|
+
|
|
5
|
+
/** Props von ViurBrowser.vue (siehe defineProps dort). */
|
|
6
|
+
export interface ViurBrowserProps {
|
|
7
|
+
/** Modulname bzw. Punkt-Pfad ("file", "shop.category"). */
|
|
8
|
+
module: string
|
|
9
|
+
/** Renderer der Requests; Standard: VITE_DEFAULT_RENDERER. */
|
|
10
|
+
renderer?: string | null
|
|
11
|
+
/** Fester Wurzelknoten; ohne ihn wird `/listRootNodes` befragt. */
|
|
12
|
+
rootNode?: string | Record<string, unknown> | null
|
|
13
|
+
/** Spalten; ohne Angabe aus der Struktur der Blätter abgeleitet. */
|
|
14
|
+
columns?: unknown[] | null
|
|
15
|
+
/** Nur diese Bones als Spalten (bei abgeleiteten Spalten). */
|
|
16
|
+
bones?: string[] | null
|
|
17
|
+
/** Zusätzliche Parameter für die Listen-Requests. */
|
|
18
|
+
params?: Record<string, unknown>
|
|
19
|
+
/** Feld der festen Reihenfolge (Standard "sortindex"). */
|
|
20
|
+
orderField?: string
|
|
21
|
+
/** Dateien in die Ebene hochladen erlauben (Datei-Module). */
|
|
22
|
+
uploads?: boolean
|
|
23
|
+
/** Wird an die DataTable durchgereicht. */
|
|
24
|
+
tableProps?: Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Ereignisse als `on*`-Props, damit `@open` & Co. im Template typen. */
|
|
28
|
+
export interface ViurBrowserEmitProps<R = Record<string, unknown>> {
|
|
29
|
+
/** Doppelklick auf ein BLATT (Ordner navigieren stattdessen hinein). */
|
|
30
|
+
onOpen?: (payload: { row: R; key: RowKey }) => void
|
|
31
|
+
/** Verschieben gespeichert. */
|
|
32
|
+
onMoved?: (payload: { keys: RowKey[]; target: RowKey | null; parentChanged?: boolean }) => void
|
|
33
|
+
onMoveFailed?: (payload: { keys: RowKey[]; target: RowKey | null; error: unknown }) => void
|
|
34
|
+
/** Uploads fertig (nur mit `uploads`). */
|
|
35
|
+
onUploaded?: (payload: { files: unknown[]; target: RowKey | null }) => void
|
|
36
|
+
onUploadFailed?: (payload: { file: File; error: unknown }) => void
|
|
37
|
+
/** v-model:location — aktueller Ort (Schlüssel des Knotens, null = Wurzel). */
|
|
38
|
+
onUpdateLocation?: (value: RowKey | null) => void
|
|
39
|
+
/** v-model:selectedKeys */
|
|
40
|
+
onUpdateSelectedKeys?: (value: RowKey[]) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Was ViurBrowser per defineExpose freigibt. */
|
|
44
|
+
export interface ViurBrowserApi<R = Record<string, unknown>> {
|
|
45
|
+
/** Der generische Browser darunter. */
|
|
46
|
+
browser: unknown
|
|
47
|
+
/** Inhalt des aktuellen Ortes (Knoten UND Blätter, je mit `skelType`). */
|
|
48
|
+
tree: {
|
|
49
|
+
data: { value: R[] }
|
|
50
|
+
structure: { value: { node?: Record<string, unknown>; leaf?: Record<string, unknown> } }
|
|
51
|
+
isFetching: { value: boolean }
|
|
52
|
+
[key: string]: unknown
|
|
53
|
+
}
|
|
54
|
+
/** Ort neu vom Server holen. */
|
|
55
|
+
refresh: () => void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare const ViurBrowser: DefineComponent<
|
|
59
|
+
ViurBrowserProps & ViurBrowserEmitProps & { location?: RowKey | null; selectedKeys?: RowKey[] },
|
|
60
|
+
ViurBrowserApi,
|
|
61
|
+
any
|
|
62
|
+
>
|
|
63
|
+
export default ViurBrowser
|
|
64
|
+
export type { BrowserMovePayload, BrowserUploadPayload }
|