update
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { featureSpeichern } from '../api.js'
|
||||
import { oeffneMenue } from '../composables/kontextmenue.js'
|
||||
import ModalBox from './ModalBox.vue'
|
||||
|
||||
const props = defineProps({
|
||||
projekt: { type: String, required: true },
|
||||
@@ -27,7 +29,7 @@ const s = ref(kopie(props.feature.sektionen))
|
||||
watch(() => props.feature, f => { s.value = kopie(f.sektionen) })
|
||||
|
||||
const aktiv = ref(null) // {feld, i, j, original} — das eine gerade offene Eingabefeld
|
||||
const gross = ref(false) // ausführliche Ansicht
|
||||
const gross = ref(props.vollmodus) // ausführliche Ansicht
|
||||
const entwurf = ref('')
|
||||
const meldung = ref('')
|
||||
const feldRef = ref(null)
|
||||
@@ -60,12 +62,7 @@ async function starte(feld, i, j) {
|
||||
}
|
||||
|
||||
function abbrechen() {
|
||||
const a = aktiv.value
|
||||
aktiv.value = null
|
||||
// frisch angelegte, noch leere Funktion wieder entfernen
|
||||
if (a && !a.original && a.feld === 'funktion') {
|
||||
s.value.funktionen.splice(a.i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function fertig() {
|
||||
@@ -73,78 +70,51 @@ async function fertig() {
|
||||
if (!a) return
|
||||
aktiv.value = null
|
||||
const neu = entwurf.value.trim()
|
||||
|
||||
if (neu === a.original) { abbrechenLeer(a); return }
|
||||
if (neu === a.original) return
|
||||
|
||||
if (a.feld === 'titel') {
|
||||
if (!neu) return // Titel ist Pflicht — leere Eingabe verwerfen
|
||||
s.value.titel = neu
|
||||
} else if (a.feld === 'funktion') {
|
||||
if (!neu) return // Funktionstitel ebenfalls; entfernt wird über das Kontextmenü
|
||||
s.value.funktionen[a.i].text = neu
|
||||
} else if (a.feld === 'ziel') {
|
||||
if (a.i === undefined) s.value.ziel = neu
|
||||
else s.value.funktionen[a.i].ziel = neu
|
||||
} else if (a.feld === 'funktion') {
|
||||
if (!neu) s.value.funktionen.splice(a.i, 1) // leeren = löschen
|
||||
else s.value.funktionen[a.i].text = neu
|
||||
} else if (a.feld === 'testen') {
|
||||
s.value.funktionen[a.i].testen = neu
|
||||
} else if (a.feld === 'regeln') {
|
||||
if (a.i === undefined) s.value.regeln = neu
|
||||
else s.value.funktionen[a.i].regeln = neu
|
||||
} else if (a.feld === 'testen') {
|
||||
s.value.funktionen[a.i].testen = neu
|
||||
}
|
||||
await speichern()
|
||||
}
|
||||
|
||||
function abbrechenLeer(a) {
|
||||
if (!a.original && a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
|
||||
}
|
||||
|
||||
// Kontextmenü: RMB auf eine Funktion (entfernen) oder auf die Karte (erstellen/Feature entfernen)
|
||||
const kontextMenue = ref(null) // {funktion: index|null, x, y}
|
||||
|
||||
function oeffneKontextMenue(e, i = null) {
|
||||
kontextMenue.value = { funktion: i, x: e.clientX, y: e.clientY }
|
||||
}
|
||||
|
||||
function schliesseKontextMenue() {
|
||||
kontextMenue.value = null
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
|
||||
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
|
||||
|
||||
// Modal für Funktion erstellen und entfernen
|
||||
// Rechtsklick auf eine Funktion (entfernen) oder auf die Karte (erstellen/Feature entfernen)
|
||||
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', i}
|
||||
const modalName = ref('')
|
||||
const modalFeld = ref(null)
|
||||
|
||||
async function menueFunktionErstellen() {
|
||||
kontextMenue.value = null
|
||||
modal.value = { art: 'erstellen' }
|
||||
modalName.value = ''
|
||||
await nextTick()
|
||||
modalFeld.value?.focus()
|
||||
function funktionsMenue(e, i) {
|
||||
oeffneMenue(e, [
|
||||
{ text: '✕ Funktion entfernen', aktion: () => (modal.value = { art: 'entfernen', i }) },
|
||||
])
|
||||
}
|
||||
|
||||
function menueFunktionEntfernen() {
|
||||
const i = kontextMenue.value.funktion
|
||||
kontextMenue.value = null
|
||||
modal.value = { art: 'entfernen', i }
|
||||
function kartenMenue(e) {
|
||||
const eintraege = []
|
||||
if (gross.value) {
|
||||
eintraege.push({ text: '+ Funktion erstellen', aktion: () => (modal.value = { art: 'erstellen' }) })
|
||||
}
|
||||
eintraege.push({ text: '✕ Feature entfernen', aktion: () => emit('entfernen') })
|
||||
oeffneMenue(e, eintraege)
|
||||
}
|
||||
|
||||
function menueFeatureEntfernen() {
|
||||
kontextMenue.value = null
|
||||
emit('entfernen')
|
||||
}
|
||||
|
||||
async function erstellenBestaetigen() {
|
||||
const name = modalName.value.trim()
|
||||
if (!name) return
|
||||
s.value.funktionen.push({ text: name, ziel: '', regeln: '', testen: '' })
|
||||
async function funktionErstellen(titel) {
|
||||
s.value.funktionen.push({ text: titel, ziel: '', regeln: '', testen: '' })
|
||||
modal.value = null
|
||||
await speichern()
|
||||
}
|
||||
|
||||
async function entfernenBestaetigen() {
|
||||
async function funktionEntfernen() {
|
||||
s.value.funktionen.splice(modal.value.i, 1)
|
||||
modal.value = null
|
||||
await speichern()
|
||||
@@ -203,10 +173,6 @@ function punkte(text) {
|
||||
function wechsleAnsicht() {
|
||||
gross.value = !gross.value
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.vollmodus) gross.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -215,7 +181,7 @@ onMounted(() => {
|
||||
:class="[s.art, { gross }]"
|
||||
@mousedown.middle.prevent
|
||||
@auxclick.middle.prevent="wechsleAnsicht"
|
||||
@contextmenu.prevent.stop="oeffneKontextMenue($event)"
|
||||
@contextmenu.prevent.stop="kartenMenue"
|
||||
>
|
||||
<header>
|
||||
<input
|
||||
@@ -282,7 +248,7 @@ onMounted(() => {
|
||||
:key="i"
|
||||
class="funktion"
|
||||
:class="{ offen: aktiv && aktiv.i === i }"
|
||||
@contextmenu.prevent.stop="oeffneKontextMenue($event, i)"
|
||||
@contextmenu.prevent.stop="funktionsMenue($event, i)"
|
||||
>
|
||||
<div class="zeile kopf">
|
||||
<span class="pfeil">▸</span>
|
||||
@@ -297,7 +263,7 @@ onMounted(() => {
|
||||
<div v-else class="funktionstitel klickbar" @click="starte('funktion', i)">{{ f.text }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Regeln und Flow nur beim Hover über die Funktion (oder während der Bearbeitung) -->
|
||||
<!-- Ziel, Regeln und Testen nur im Vollmodus -->
|
||||
<div class="details">
|
||||
<!-- Ziel der Funktion -->
|
||||
<div v-if="istAktiv('ziel', i) || f.ziel" class="block">
|
||||
@@ -357,44 +323,21 @@ onMounted(() => {
|
||||
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
|
||||
<div
|
||||
v-if="kontextMenue"
|
||||
class="kontextmenue"
|
||||
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||
>
|
||||
<button v-if="kontextMenue.funktion !== null" @click="menueFunktionEntfernen">✕ Funktion entfernen</button>
|
||||
<template v-else>
|
||||
<button v-if="gross" @click="menueFunktionErstellen">+ Funktion erstellen</button>
|
||||
<button @click="menueFeatureEntfernen">✕ Feature entfernen</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="modal" class="schleier" @click.self="modal = null">
|
||||
<div class="modal karte">
|
||||
<template v-if="modal.art === 'erstellen'">
|
||||
<h2>Funktion erstellen</h2>
|
||||
<input
|
||||
ref="modalFeld"
|
||||
v-model="modalName"
|
||||
placeholder="Funktionstitel"
|
||||
@keyup.enter="erstellenBestaetigen"
|
||||
@keydown.esc="modal = null"
|
||||
>
|
||||
<footer>
|
||||
<button @click="modal = null">Abbrechen</button>
|
||||
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2>Funktion entfernen</h2>
|
||||
<p class="frage">„{{ s.funktionen[modal.i].text }}“ entfernen?</p>
|
||||
<footer>
|
||||
<button @click="modal = null">Abbrechen</button>
|
||||
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
|
||||
</footer>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ModalBox
|
||||
v-if="modal && modal.art === 'erstellen'"
|
||||
titel="Funktion erstellen"
|
||||
platzhalter="Funktionstitel"
|
||||
@bestaetigt="funktionErstellen"
|
||||
@abbrechen="modal = null"
|
||||
/>
|
||||
<ModalBox
|
||||
v-else-if="modal"
|
||||
titel="Funktion entfernen"
|
||||
:frage="`„${s.funktionen[modal.i].text}“ entfernen?`"
|
||||
knopf="Entfernen"
|
||||
@bestaetigt="funktionEntfernen"
|
||||
@abbrechen="modal = null"
|
||||
/>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@@ -402,8 +345,8 @@ onMounted(() => {
|
||||
.karte.kern { border-color: var(--akzent); }
|
||||
.karte.optional { border-color: var(--gruen); }
|
||||
|
||||
/* Ausführliche Ansicht: volle Breite, Funktionen in Spalten, alle Details offen.
|
||||
Jede Funktion als eigene Box, Zufüge-Knöpfe nur beim Hover über die Box. */
|
||||
/* Vollmodus: volle Breite, Funktionen als Boxen in responsiven Spalten,
|
||||
alle Details offen */
|
||||
.karte.gross { column-span: all; display: block; }
|
||||
.karte.gross .liste { columns: 320px; column-gap: 1.2rem; }
|
||||
.karte.gross section.leer { display: block; }
|
||||
@@ -454,11 +397,6 @@ section + section { border-top: 1px solid var(--rand); }
|
||||
|
||||
/* Leere Sektionen nur in der ausführlichen Ansicht */
|
||||
section.leer { display: none; }
|
||||
.werkzeug { opacity: 0; transition: opacity 0.1s; }
|
||||
.karte:hover .werkzeug, .werkzeug:focus { opacity: 1; }
|
||||
/* „+ Funktion" belegt ohne Hover auch keinen Platz */
|
||||
.hinzu.werkzeug { display: none; }
|
||||
.karte:hover .hinzu.werkzeug { display: block; }
|
||||
|
||||
.hinweis {
|
||||
color: var(--dezent);
|
||||
@@ -484,9 +422,6 @@ section.leer { display: none; }
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.zeile .inhalt, .zeile input, .zeile textarea { flex: 1; min-width: 0; }
|
||||
/* ✕ nur auf der Zeile, über der die Maus steht */
|
||||
.zeile .icon-knopf { font-size: 0.8rem; opacity: 0; }
|
||||
.zeile:hover .icon-knopf { opacity: 1; }
|
||||
|
||||
/* Funktionsliste */
|
||||
.zeile.kopf { padding: 0.15rem 0.2rem; border-radius: 4px; }
|
||||
@@ -494,10 +429,10 @@ section.leer { display: none; }
|
||||
.funktionstitel { flex: 1; }
|
||||
.pfeil { color: var(--dezent); font-size: 0.75rem; }
|
||||
|
||||
/* Regeln und Flow nur in der ausführlichen Ansicht */
|
||||
/* Ziel, Regeln und Testen nur im Vollmodus */
|
||||
.details { display: none; font-size: 0.9rem; }
|
||||
|
||||
/* Regeln (Punktliste) und Flow (nummerierte Schritte) als getönte Blöcke */
|
||||
/* Regeln (Punktliste) und Testen (nummerierte Schritte) als getönte Blöcke */
|
||||
.block {
|
||||
background: var(--code-bg);
|
||||
border-radius: 6px;
|
||||
@@ -530,7 +465,7 @@ section.leer { display: none; }
|
||||
color: var(--dezent);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
/* „+ Regeln"/„+ Flow" bündig zur Listen-Einrückung */
|
||||
/* „+ Regeln"/„+ Testen" bündig zur Listen-Einrückung */
|
||||
.details .hinzu {
|
||||
display: block;
|
||||
padding: 0.05rem 0 0.05rem 0.55rem;
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { fetchFeatures, featureAnlegen, featureLoeschen } from '../api.js'
|
||||
import { oeffneMenue } from '../composables/kontextmenue.js'
|
||||
import FeatureKarte from './FeatureKarte.vue'
|
||||
import ModalBox from './ModalBox.vue'
|
||||
|
||||
const props = defineProps({
|
||||
projekt: { type: String, required: true },
|
||||
})
|
||||
const emit = defineEmits(['geaendert'])
|
||||
|
||||
const features = ref([])
|
||||
const meldung = ref('')
|
||||
const neuId = ref(null) // frisch angelegte Karte: direkt im Vollmodus öffnen
|
||||
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', feature}
|
||||
const neuKey = ref(null) // frisch angelegte Karte: direkt im Vollmodus öffnen
|
||||
|
||||
// Stabiler Key je Karte: die id ändert sich mit dem Titel, ein Wechsel würde
|
||||
// die Karte neu aufbauen und dabei den Vollmodus verlieren.
|
||||
let letzterKey = 0
|
||||
const mitKey = (f) => ({ ...f, key: ++letzterKey })
|
||||
|
||||
function sortiere() {
|
||||
features.value.sort((a, b) =>
|
||||
a.sektionen.titel.localeCompare(b.sektionen.titel, 'de', { sensitivity: 'base' }))
|
||||
}
|
||||
|
||||
async function laden() {
|
||||
meldung.value = ''
|
||||
neuId.value = null
|
||||
neuKey.value = null
|
||||
try {
|
||||
features.value = await fetchFeatures(props.projekt)
|
||||
features.value = (await fetchFeatures(props.projekt)).map(mitKey)
|
||||
} catch (e) {
|
||||
features.value = []
|
||||
meldung.value = e.message
|
||||
@@ -25,65 +37,38 @@ async function laden() {
|
||||
|
||||
function aktualisiert(altId, f) {
|
||||
const idx = features.value.findIndex(x => x.id === altId)
|
||||
if (idx >= 0) features.value[idx] = f
|
||||
if (neuId.value === altId) neuId.value = f.id
|
||||
if (idx < 0) return
|
||||
features.value[idx] = { ...f, key: features.value[idx].key }
|
||||
sortiere()
|
||||
}
|
||||
|
||||
// Kontextmenü auf dem Hauptbereich (erstellen); das Menü der Card führt die Karte selbst
|
||||
const kontextMenue = ref(null) // {x, y}
|
||||
|
||||
function oeffneBereichsMenue(e) {
|
||||
kontextMenue.value = { x: e.clientX, y: e.clientY }
|
||||
function bereichsMenue(e) {
|
||||
oeffneMenue(e, [{ text: '+ Feature erstellen', aktion: () => oeffneModal({ art: 'erstellen' }) }])
|
||||
}
|
||||
|
||||
function schliesseKontextMenue() {
|
||||
kontextMenue.value = null
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
|
||||
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
|
||||
|
||||
// Modal für Erstellen und Entfernen
|
||||
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', feature}
|
||||
const modalName = ref('')
|
||||
const modalFeld = ref(null)
|
||||
|
||||
async function menueErstellen() {
|
||||
kontextMenue.value = null
|
||||
function oeffneModal(m) {
|
||||
meldung.value = ''
|
||||
modal.value = { art: 'erstellen' }
|
||||
modalName.value = ''
|
||||
await nextTick()
|
||||
modalFeld.value?.focus()
|
||||
modal.value = m
|
||||
}
|
||||
|
||||
function featureEntfernen(feature) {
|
||||
meldung.value = ''
|
||||
modal.value = { art: 'entfernen', feature }
|
||||
}
|
||||
|
||||
async function erstellenBestaetigen() {
|
||||
meldung.value = ''
|
||||
const name = modalName.value.trim()
|
||||
if (!name) return
|
||||
async function erstellen(titel) {
|
||||
try {
|
||||
const f = await featureAnlegen(props.projekt, `<!-- titel -->\n${name}\n`)
|
||||
const f = mitKey(await featureAnlegen(props.projekt, `<!-- titel -->\n${titel}\n`))
|
||||
modal.value = null
|
||||
features.value.push(f)
|
||||
neuId.value = f.id // öffnet die Karte im Vollmodus
|
||||
emit('geaendert')
|
||||
sortiere()
|
||||
neuKey.value = f.key // öffnet die Karte im Vollmodus
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function entfernenBestaetigen() {
|
||||
meldung.value = ''
|
||||
async function entfernen() {
|
||||
const id = modal.value.feature.id
|
||||
try {
|
||||
await featureLoeschen(props.projekt, modal.value.feature.id)
|
||||
features.value = features.value.filter(f => f.id !== modal.value.feature.id)
|
||||
await featureLoeschen(props.projekt, id)
|
||||
features.value = features.value.filter(f => f.id !== id)
|
||||
modal.value = null
|
||||
emit('geaendert')
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
@@ -93,57 +78,38 @@ watch(() => props.projekt, laden, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bereich" @contextmenu.prevent="oeffneBereichsMenue">
|
||||
<div class="bereich" @contextmenu.prevent="bereichsMenue">
|
||||
<p v-if="meldung && !modal" class="meldung">{{ meldung }}</p>
|
||||
|
||||
<div class="gitter">
|
||||
<FeatureKarte
|
||||
v-for="f in features"
|
||||
:key="f.id"
|
||||
:key="f.key"
|
||||
:projekt="projekt"
|
||||
:feature="f"
|
||||
:vollmodus="f.id === neuId"
|
||||
:vollmodus="f.key === neuKey"
|
||||
@aktualisiert="aktualisiert"
|
||||
@entfernen="featureEntfernen(f)"
|
||||
@entfernen="oeffneModal({ art: 'entfernen', feature: f })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="kontextMenue"
|
||||
class="kontextmenue"
|
||||
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||
>
|
||||
<button @click="menueErstellen">+ Feature erstellen</button>
|
||||
</div>
|
||||
|
||||
<div v-if="modal" class="schleier" @click.self="modal = null">
|
||||
<div class="modal karte">
|
||||
<template v-if="modal.art === 'erstellen'">
|
||||
<h2>Feature erstellen</h2>
|
||||
<input
|
||||
ref="modalFeld"
|
||||
v-model="modalName"
|
||||
placeholder="Feature-Titel"
|
||||
@keyup.enter="erstellenBestaetigen"
|
||||
@keydown.esc="modal = null"
|
||||
>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
<footer>
|
||||
<button @click="modal = null">Abbrechen</button>
|
||||
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2>Feature entfernen</h2>
|
||||
<p class="frage">„{{ modal.feature.sektionen.titel }}“ entfernen?</p>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
<footer>
|
||||
<button @click="modal = null">Abbrechen</button>
|
||||
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
|
||||
</footer>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ModalBox
|
||||
v-if="modal && modal.art === 'erstellen'"
|
||||
titel="Feature erstellen"
|
||||
platzhalter="Feature-Titel"
|
||||
:meldung="meldung"
|
||||
@bestaetigt="erstellen"
|
||||
@abbrechen="modal = null"
|
||||
/>
|
||||
<ModalBox
|
||||
v-else-if="modal"
|
||||
titel="Feature entfernen"
|
||||
:frage="`„${modal.feature.sektionen.titel}“ entfernen?`"
|
||||
knopf="Entfernen"
|
||||
:meldung="meldung"
|
||||
@bestaetigt="entfernen"
|
||||
@abbrechen="modal = null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
18
frontend/src/components/KontextMenue.vue
Normal file
18
frontend/src/components/KontextMenue.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { kontextMenue, schliesseMenue } from '../composables/kontextmenue.js'
|
||||
|
||||
function waehle(eintrag) {
|
||||
schliesseMenue()
|
||||
eintrag.aktion()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="kontextMenue"
|
||||
class="kontextmenue"
|
||||
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||
>
|
||||
<button v-for="e in kontextMenue.eintraege" :key="e.text" @click="waehle(e)">{{ e.text }}</button>
|
||||
</div>
|
||||
</template>
|
||||
44
frontend/src/components/ModalBox.vue
Normal file
44
frontend/src/components/ModalBox.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
titel: { type: String, required: true },
|
||||
frage: { type: String, default: '' }, // gesetzt = Rückfrage statt Eingabe
|
||||
platzhalter: { type: String, default: '' },
|
||||
knopf: { type: String, default: 'Bestätigen' },
|
||||
meldung: { type: String, default: '' },
|
||||
})
|
||||
const emit = defineEmits(['bestaetigt', 'abbrechen'])
|
||||
|
||||
const wert = ref('')
|
||||
const feld = ref(null)
|
||||
|
||||
function bestaetigen() {
|
||||
if (!props.frage && !wert.value.trim()) return
|
||||
emit('bestaetigt', wert.value.trim())
|
||||
}
|
||||
|
||||
onMounted(() => feld.value?.focus())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="schleier" @click.self="emit('abbrechen')">
|
||||
<div class="modal karte">
|
||||
<h2>{{ titel }}</h2>
|
||||
<p v-if="frage" class="frage">{{ frage }}</p>
|
||||
<input
|
||||
v-else
|
||||
ref="feld"
|
||||
v-model="wert"
|
||||
:placeholder="platzhalter"
|
||||
@keyup.enter="bestaetigen"
|
||||
@keydown.esc="emit('abbrechen')"
|
||||
>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
<footer>
|
||||
<button @click="emit('abbrechen')">Abbrechen</button>
|
||||
<button class="primaer" :disabled="!frage && !wert.trim()" @click="bestaetigen">{{ knopf }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { projektAnlegen, projektUmbenennen, projektLoeschen } from '../api.js'
|
||||
import { oeffneMenue } from '../composables/kontextmenue.js'
|
||||
import ModalBox from './ModalBox.vue'
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
projekte: { type: Array, required: true },
|
||||
aktiv: { type: String, default: null },
|
||||
})
|
||||
@@ -12,6 +14,7 @@ const meldung = ref('')
|
||||
const bearbeitet = ref(null) // Projektname, der gerade umbenannt wird
|
||||
const umbenennung = ref('')
|
||||
const umbenennFeld = ref(null)
|
||||
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', name}
|
||||
|
||||
async function starteUmbenennen(name) {
|
||||
meldung.value = ''
|
||||
@@ -33,56 +36,24 @@ async function umbenennen(alt) {
|
||||
}
|
||||
}
|
||||
|
||||
// Kontextmenü: Rechtsklick auf Projektname (anpassen/entfernen)
|
||||
// oder auf freie Sidebar-Fläche (erstellen)
|
||||
const kontextMenue = ref(null) // {name|null, x, y}
|
||||
|
||||
function oeffneProjektMenue(e, name) {
|
||||
kontextMenue.value = { name, x: e.clientX, y: e.clientY }
|
||||
// Rechtsklick auf einen Projektnamen (anpassen/entfernen) oder auf die Sidebar (erstellen)
|
||||
function projektMenue(e, name) {
|
||||
oeffneMenue(e, [
|
||||
{ text: '✎ Projekt anpassen', aktion: () => starteUmbenennen(name) },
|
||||
{ text: '✕ Projekt entfernen', aktion: () => oeffneModal({ art: 'entfernen', name }) },
|
||||
])
|
||||
}
|
||||
|
||||
function oeffneLeistenMenue(e) {
|
||||
kontextMenue.value = { name: null, x: e.clientX, y: e.clientY }
|
||||
function leistenMenue(e) {
|
||||
oeffneMenue(e, [{ text: '+ Projekt erstellen', aktion: () => oeffneModal({ art: 'erstellen' }) }])
|
||||
}
|
||||
|
||||
function schliesseKontextMenue() {
|
||||
kontextMenue.value = null
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('click', schliesseKontextMenue))
|
||||
onUnmounted(() => window.removeEventListener('click', schliesseKontextMenue))
|
||||
|
||||
// Modal für Erstellen und Entfernen
|
||||
const modal = ref(null) // {art: 'erstellen'} | {art: 'entfernen', name}
|
||||
const modalName = ref('')
|
||||
const modalFeld = ref(null)
|
||||
|
||||
async function menueErstellen() {
|
||||
kontextMenue.value = null
|
||||
function oeffneModal(m) {
|
||||
meldung.value = ''
|
||||
modal.value = { art: 'erstellen' }
|
||||
modalName.value = ''
|
||||
await nextTick()
|
||||
modalFeld.value?.focus()
|
||||
modal.value = m
|
||||
}
|
||||
|
||||
function menueAnpassen() {
|
||||
const name = kontextMenue.value.name
|
||||
kontextMenue.value = null
|
||||
starteUmbenennen(name)
|
||||
}
|
||||
|
||||
function menueEntfernen() {
|
||||
const name = kontextMenue.value.name
|
||||
kontextMenue.value = null
|
||||
meldung.value = ''
|
||||
modal.value = { art: 'entfernen', name }
|
||||
}
|
||||
|
||||
async function erstellenBestaetigen() {
|
||||
meldung.value = ''
|
||||
const name = modalName.value.trim()
|
||||
if (!name) return
|
||||
async function erstellen(name) {
|
||||
try {
|
||||
await projektAnlegen(name)
|
||||
modal.value = null
|
||||
@@ -93,8 +64,7 @@ async function erstellenBestaetigen() {
|
||||
}
|
||||
}
|
||||
|
||||
async function entfernenBestaetigen() {
|
||||
meldung.value = ''
|
||||
async function entfernen() {
|
||||
try {
|
||||
await projektLoeschen(modal.value.name)
|
||||
modal.value = null
|
||||
@@ -106,7 +76,7 @@ async function entfernenBestaetigen() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside @contextmenu.prevent="oeffneLeistenMenue">
|
||||
<aside @contextmenu.prevent="leistenMenue">
|
||||
<header>
|
||||
<p class="gruppe">Projekte</p>
|
||||
<slot />
|
||||
@@ -117,65 +87,39 @@ async function entfernenBestaetigen() {
|
||||
:key="p.name"
|
||||
:class="{ aktiv: p.name === aktiv }"
|
||||
@click="emit('auswaehlen', p.name)"
|
||||
@contextmenu.prevent.stop="oeffneProjektMenue($event, p.name)"
|
||||
@contextmenu.prevent.stop="projektMenue($event, p.name)"
|
||||
>
|
||||
<template v-if="bearbeitet === p.name">
|
||||
<input
|
||||
ref="umbenennFeld"
|
||||
v-model="umbenennung"
|
||||
@keyup.enter="umbenennen(p.name)"
|
||||
@keyup.escape="bearbeitet = null"
|
||||
@blur="umbenennen(p.name)"
|
||||
@click.stop
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="name">{{ p.name }}</span>
|
||||
</template>
|
||||
<input
|
||||
v-if="bearbeitet === p.name"
|
||||
ref="umbenennFeld"
|
||||
v-model="umbenennung"
|
||||
@keyup.enter="umbenennen(p.name)"
|
||||
@keydown.esc="bearbeitet = null"
|
||||
@blur="umbenennen(p.name)"
|
||||
@click.stop
|
||||
>
|
||||
<span v-else class="name">{{ p.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="meldung && !modal" class="meldung">{{ meldung }}</p>
|
||||
|
||||
<div
|
||||
v-if="kontextMenue"
|
||||
class="kontextmenue"
|
||||
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||
>
|
||||
<template v-if="kontextMenue.name">
|
||||
<button @click="menueAnpassen">✎ Projekt anpassen</button>
|
||||
<button @click="menueEntfernen">✕ Projekt entfernen</button>
|
||||
</template>
|
||||
<button v-else @click="menueErstellen">+ Projekt erstellen</button>
|
||||
</div>
|
||||
|
||||
<div v-if="modal" class="schleier" @click.self="modal = null">
|
||||
<div class="modal karte">
|
||||
<template v-if="modal.art === 'erstellen'">
|
||||
<h2>Projekt erstellen</h2>
|
||||
<input
|
||||
ref="modalFeld"
|
||||
v-model="modalName"
|
||||
placeholder="Projektname"
|
||||
@keyup.enter="erstellenBestaetigen"
|
||||
@keydown.esc="modal = null"
|
||||
>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
<footer>
|
||||
<button @click="modal = null">Abbrechen</button>
|
||||
<button class="primaer" :disabled="!modalName.trim()" @click="erstellenBestaetigen">Bestätigen</button>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2>Projekt entfernen</h2>
|
||||
<p class="frage">„{{ modal.name }}“ mit allen Features entfernen?</p>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
<footer>
|
||||
<button @click="modal = null">Abbrechen</button>
|
||||
<button class="primaer" @click="entfernenBestaetigen">Entfernen</button>
|
||||
</footer>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<ModalBox
|
||||
v-if="modal && modal.art === 'erstellen'"
|
||||
titel="Projekt erstellen"
|
||||
platzhalter="Projektname"
|
||||
:meldung="meldung"
|
||||
@bestaetigt="erstellen"
|
||||
@abbrechen="modal = null"
|
||||
/>
|
||||
<ModalBox
|
||||
v-else-if="modal"
|
||||
titel="Projekt entfernen"
|
||||
:frage="`„${modal.name}“ mit allen Features entfernen?`"
|
||||
knopf="Entfernen"
|
||||
:meldung="meldung"
|
||||
@bestaetigt="entfernen"
|
||||
@abbrechen="modal = null"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -220,5 +164,4 @@ li.aktiv { background: var(--bg); font-weight: 600; }
|
||||
white-space: nowrap;
|
||||
}
|
||||
li input { width: 100%; padding: 0.1rem 0.4rem; }
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user