762 lines
21 KiB
JavaScript
762 lines
21 KiB
JavaScript
// =============================
|
||
// SETUP: Wichtige HTML-Elemente holen
|
||
// Diese Konstanten verbinden unser JavaScript mit dem HTML.
|
||
// So können wir später Buttons, Formularfelder und Bereiche steuern.
|
||
// =============================
|
||
const form = document.getElementById("eventForm");
|
||
const steps = Array.from(document.querySelectorAll(".step"));
|
||
const backButton = document.getElementById("backButton");
|
||
const nextButton = document.getElementById("nextButton");
|
||
const progressBar = document.getElementById("progressBar");
|
||
const progressMarker = document.getElementById("progressMarker");
|
||
const progressMarkerLabel = document.getElementById("progressMarkerLabel");
|
||
const errorMessage = document.getElementById("errorMessage");
|
||
const usernameElement = document.getElementById("username");
|
||
const flowFooter = document.getElementById("flowFooter");
|
||
const submissionSuccess = document.getElementById("submissionSuccess");
|
||
const EVENTS_STORAGE_KEY = "socialCookingEvents";
|
||
|
||
// =============================
|
||
// STATE: aktueller Schritt im Flow
|
||
// currentStep merkt sich, auf welchem Schritt der User gerade ist.
|
||
// lastStep ist der letzte Schritt im Formular.
|
||
// =============================
|
||
let currentStep = 0;
|
||
const lastStep = steps.length - 1;
|
||
|
||
// Text für den Weiter-Button je nach Schritt
|
||
const nextLabels = {
|
||
0: "Weiter",
|
||
1: "Weiter",
|
||
2: "Weiter",
|
||
3: "Weiter",
|
||
4: "Weiter",
|
||
5: "Weiter",
|
||
6: "Weiter",
|
||
7: "Event veröffentlichen"
|
||
};
|
||
|
||
// Demo-Wert: Später könnte der Name z. B. aus einem User-Profil kommen
|
||
usernameElement.textContent = "Mia";
|
||
|
||
|
||
// =============================
|
||
// STEP 1: Kleine Hilfsfunktionen
|
||
// Diese Funktionen helfen uns später an mehreren Stellen im Code.
|
||
// =============================
|
||
|
||
/**
|
||
* Gibt alle Eingabefelder eines bestimmten Schritts zurück.
|
||
* Rückgabe: Array mit input-, textarea- und select-Feldern.
|
||
*/
|
||
function getStepFields(stepIndex) {
|
||
return Array.from(
|
||
steps[stepIndex].querySelectorAll("input, textarea, select")
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Zeigt eine Fehlermeldung im Formular an.
|
||
* Wenn keine Nachricht übergeben wird, wird die Meldung geleert.
|
||
*/
|
||
function setErrorMessage(message = "") {
|
||
errorMessage.textContent = message;
|
||
}
|
||
|
||
/**
|
||
* Entfernt alle Fehlermarkierungen innerhalb eines Schritts.
|
||
*/
|
||
function clearStepInvalidState(stepIndex) {
|
||
if (!steps[stepIndex]) return;
|
||
|
||
steps[stepIndex]
|
||
.querySelectorAll(".field-invalid, .option-card--invalid")
|
||
.forEach(element => {
|
||
element.classList.remove("field-invalid", "option-card--invalid");
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Markiert ein einzelnes Feld visuell als ungültig.
|
||
*/
|
||
function markFieldInvalid(field) {
|
||
field.classList.add("field-invalid");
|
||
}
|
||
|
||
/**
|
||
* Markiert eine ganze Radio-Gruppe visuell als ungültig.
|
||
*/
|
||
function markRadioGroupInvalid(group) {
|
||
group.forEach(field => {
|
||
const card = field.closest(".option-card");
|
||
|
||
if (card) {
|
||
card.classList.add("option-card--invalid");
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 2: Schritt anzeigen & Oberfläche aktualisieren
|
||
// showStep() ist eine der wichtigsten Funktionen:
|
||
// Sie bestimmt, welcher Schritt sichtbar ist,
|
||
// und aktualisiert gleichzeitig die restliche Oberfläche.
|
||
// =============================
|
||
|
||
/**
|
||
* Zeigt den gewünschten Schritt an.
|
||
* Dabei werden auch Buttons, Progress Bar und Review aktualisiert.
|
||
*/
|
||
function showStep(index) {
|
||
currentStep = index;
|
||
submissionSuccess.hidden = true;
|
||
clearStepInvalidState(index);
|
||
|
||
// Nur der aktuelle Schritt soll sichtbar sein
|
||
steps.forEach((step, stepIndex) => {
|
||
step.classList.toggle("step--active", stepIndex === index);
|
||
});
|
||
|
||
updateFlowVisibility(index);
|
||
updateReviewIfNeeded(index);
|
||
updateProgressBar(index, lastStep);
|
||
setErrorMessage("");
|
||
|
||
// Für bessere UX: bei jedem Schritt wieder nach oben scrollen
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
}
|
||
|
||
/**
|
||
* Steuert Sichtbarkeit von Footer und Buttons.
|
||
* Im Intro-Schritt werden Footer und Zurück-Button versteckt.
|
||
* Zusätzlich bekommt der Weiter-Button je nach Schritt ein anderes Label.
|
||
*/
|
||
function updateFlowVisibility(stepIndex) {
|
||
const isIntroStep = stepIndex === 0;
|
||
|
||
flowFooter.hidden = isIntroStep;
|
||
backButton.hidden = isIntroStep;
|
||
nextButton.textContent = nextLabels[stepIndex];
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 3: Fortschrittsanzeige
|
||
// Die Progress Bar zeigt, wie weit der User im Flow ist.
|
||
// Das Intro zählt noch nicht als eigentlicher Formularschritt.
|
||
// =============================
|
||
|
||
/**
|
||
* Berechnet den Fortschritt in Prozent und setzt die Breite der Progress Bar.
|
||
* Beispiel:
|
||
* - Intro = 0%
|
||
* - erster echter Formularschritt = 0%
|
||
* - letzter Schritt = 100%
|
||
*/
|
||
function updateProgressBar(stepIndex, totalStepIndex) {
|
||
let progress = 0;
|
||
let markerPosition = 0;
|
||
let markerStep = 1;
|
||
let markerTransform = "translateX(-50%)";
|
||
|
||
if (stepIndex > 0) {
|
||
progress = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
|
||
markerPosition = ((stepIndex - 1) / (totalStepIndex - 1)) * 100;
|
||
markerStep = stepIndex;
|
||
}
|
||
|
||
progressBar.style.width = `${progress}%`;
|
||
progressMarker.style.left = `${markerPosition}%`;
|
||
progressMarker.style.transform = markerTransform;
|
||
progressMarker.hidden = stepIndex === 0;
|
||
progressMarkerLabel.textContent = String(markerStep);
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 4: Review-Daten vorbereiten
|
||
// Im letzten Schritt werden alle eingegebenen Daten nochmals angezeigt.
|
||
// Dafür brauchen wir Funktionen, die Feldwerte sauber auslesen und formatieren.
|
||
// =============================
|
||
|
||
/**
|
||
* Führt das Update der Review nur aus,
|
||
* wenn wirklich der letzte Schritt geöffnet ist.
|
||
*/
|
||
function updateReviewIfNeeded(stepIndex) {
|
||
if (stepIndex === lastStep) {
|
||
updateReview();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gibt den Wert eines Formularfeldes zurück.
|
||
* Rückgabe:
|
||
* - eingegebener Text / ausgewählte Option
|
||
* - oder "–", falls nichts vorhanden ist
|
||
*/
|
||
function getFieldValue(name) {
|
||
const field = form.elements[name];
|
||
|
||
if (!field) return "–";
|
||
|
||
// Spezialfall: Radio-Gruppen verhalten sich anders als normale Inputs
|
||
if (field instanceof RadioNodeList) {
|
||
return field.value || "–";
|
||
}
|
||
|
||
return field.value.trim() || "–";
|
||
}
|
||
|
||
/**
|
||
* Gibt alle ausgewählten Checkbox-Werte als Text zurück.
|
||
* Beispiel: "Vegetarisch, Vegan"
|
||
* Falls nichts ausgewählt wurde: "Keine Angabe"
|
||
*/
|
||
function getCheckboxValues(name) {
|
||
const checked = Array.from(
|
||
form.querySelectorAll(`input[name="${name}"]:checked`)
|
||
);
|
||
|
||
return checked.length
|
||
? checked.map(item => item.value).join(", ")
|
||
: "Keine Angabe";
|
||
}
|
||
|
||
/**
|
||
* Formatiert ein Datum für die Review-Anzeige.
|
||
* Beispiel: aus "2026-03-26" wird "26.03.2026"
|
||
*/
|
||
function formatDate(value) {
|
||
if (!value || value === "–") return "–";
|
||
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return value;
|
||
|
||
return new Intl.DateTimeFormat("de-CH", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric"
|
||
}).format(date);
|
||
}
|
||
|
||
/**
|
||
* Schreibt einen Wert in das passende Feld der Review-Ansicht.
|
||
* Gesucht wird ein HTML-Element mit data-review="..."
|
||
*/
|
||
function updateReviewField(fieldName, value) {
|
||
const target = document.querySelector(`[data-review="${fieldName}"]`);
|
||
|
||
if (target) {
|
||
target.textContent = value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Baut den Text für Allergien / Hinweise zusammen.
|
||
* Dabei werden Checkboxen und zusätzliches Freitextfeld kombiniert.
|
||
*/
|
||
function buildAllergiesReviewValue() {
|
||
const selected = getCheckboxValues("allergies");
|
||
const notes = getFieldValue("allergiesOther");
|
||
|
||
if (selected === "Keine Angabe" && notes === "–") return "Keine Angabe";
|
||
|
||
if (selected !== "Keine Angabe" && notes !== "–") {
|
||
return `${selected}, ${notes}`;
|
||
}
|
||
|
||
return selected !== "Keine Angabe" ? selected : notes;
|
||
}
|
||
|
||
/**
|
||
* Liest alle wichtigen Formularwerte aus
|
||
* und schreibt sie gesammelt in die Review-Ansicht.
|
||
*/
|
||
function updateReview() {
|
||
const reviewValues = getReviewValues();
|
||
|
||
Object.entries(reviewValues).forEach(([key, value]) => {
|
||
updateReviewField(key, value);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Liest alle wichtigen Formularwerte gesammelt aus.
|
||
*/
|
||
function getReviewValues() {
|
||
return {
|
||
eventTitle: getFieldValue("eventTitle"),
|
||
eventType: getFieldValue("eventType"),
|
||
menuDescription: getFieldValue("menuDescription"),
|
||
eventDescription: getFieldValue("eventDescription"),
|
||
maxGuests: getFieldValue("maxGuests"),
|
||
dietType: getFieldValue("dietType"),
|
||
allergies: buildAllergiesReviewValue(),
|
||
eventDate: formatDate(getFieldValue("eventDate")),
|
||
eventTime: getFieldValue("eventTime"),
|
||
eventAddress: getFieldValue("eventAddress"),
|
||
eventCity: getFieldValue("eventCity")
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Liest lokal gespeicherte Events robust aus dem Browser-Storage.
|
||
*/
|
||
function getStoredEvents() {
|
||
try {
|
||
const stored = localStorage.getItem(EVENTS_STORAGE_KEY);
|
||
return stored ? JSON.parse(stored) : [];
|
||
} catch (error) {
|
||
console.error("Lokale Events konnten nicht gelesen werden:", error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Speichert die komplette Eventliste zurück in den Browser-Storage.
|
||
*/
|
||
function setStoredEvents(events) {
|
||
localStorage.setItem(EVENTS_STORAGE_KEY, JSON.stringify(events));
|
||
}
|
||
|
||
/**
|
||
* Formatiert ein ISO-Datum in das bestehende Eventformat der Demo-Daten.
|
||
*/
|
||
function formatDateForStorage(value) {
|
||
if (!value) return "";
|
||
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return value;
|
||
|
||
const monthMap = {
|
||
0: "JAN",
|
||
1: "FEB",
|
||
2: "MRZ",
|
||
3: "APR",
|
||
4: "MAI",
|
||
5: "JUN",
|
||
6: "JUL",
|
||
7: "AUG",
|
||
8: "SEP",
|
||
9: "OKT",
|
||
10: "NOV",
|
||
11: "DEZ"
|
||
};
|
||
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
const month = monthMap[date.getMonth()];
|
||
const year = date.getFullYear();
|
||
|
||
return `${day}. ${month}. ${year}`;
|
||
}
|
||
|
||
/**
|
||
* Formatiert die Zeit in das bestehende Eventformat der Demo-Daten.
|
||
*/
|
||
function formatTimeForStorage(value) {
|
||
return value ? `${value} UHR` : "";
|
||
}
|
||
|
||
/**
|
||
* Zerlegt das Menü-Textarea in saubere Listenpunkte.
|
||
*/
|
||
function buildMenuItems(value) {
|
||
return value
|
||
.split("\n")
|
||
.map(item => item.replace(/^[•-]\s*/, "").trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
/**
|
||
* Leitet den gewählten Eventtyp in die Kategorien der Übersicht über.
|
||
*/
|
||
function mapEventTypeToCategory(value) {
|
||
const categoryMap = {
|
||
Brunch: "BRUNCH",
|
||
Lunch: "LUNCH",
|
||
Dinner: "DINNER",
|
||
"Kaffee + Kuchen": "COFFEE"
|
||
};
|
||
|
||
return categoryMap[value] || value.toUpperCase();
|
||
}
|
||
|
||
/**
|
||
* Baut aus den Formulardaten ein lokal speicherbares Event-Objekt.
|
||
*/
|
||
function buildStoredEvent() {
|
||
const eventType = getFieldValue("eventType");
|
||
const dietType = getFieldValue("dietType");
|
||
const menuDescription = form.elements.menuDescription.value.trim();
|
||
const eventDescription = form.elements.eventDescription.value.trim();
|
||
const eventDate = form.elements.eventDate.value;
|
||
const eventTime = form.elements.eventTime.value;
|
||
const eventCity = form.elements.eventCity.value.trim();
|
||
|
||
return {
|
||
id: Date.now(),
|
||
title: form.elements.eventTitle.value.trim(),
|
||
location: eventCity,
|
||
address: form.elements.eventAddress.value.trim(),
|
||
date: formatDateForStorage(eventDate),
|
||
time: formatTimeForStorage(eventTime),
|
||
category: mapEventTypeToCategory(eventType),
|
||
diet: dietType,
|
||
spots: Number(form.elements.maxGuests.value),
|
||
host: {
|
||
name: usernameElement.textContent.trim() || "Host",
|
||
initial: (usernameElement.textContent.trim().charAt(0) || "H").toUpperCase()
|
||
},
|
||
hostMessage: [eventDescription],
|
||
menu: buildMenuItems(menuDescription),
|
||
specifications: getCheckboxValues("allergies") === "Keine Angabe"
|
||
? []
|
||
: getCheckboxValues("allergies").split(", ").filter(Boolean),
|
||
allergiesNote: form.elements.allergiesOther.value.trim(),
|
||
participants: [usernameElement.textContent.trim() || "Host"],
|
||
gallery: [],
|
||
createdAt: new Date().toISOString(),
|
||
source: "local"
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Speichert das aktuell erstellte Event lokal im Browser.
|
||
*/
|
||
function saveCurrentEvent() {
|
||
const storedEvents = getStoredEvents();
|
||
const nextEvents = [buildStoredEvent(), ...storedEvents];
|
||
setStoredEvents(nextEvents);
|
||
}
|
||
|
||
// =============================
|
||
// STEP 5: Validierung
|
||
// Bevor der User weitergehen darf, prüfen wir:
|
||
// 1. Sind Pflichtfelder ausgefüllt?
|
||
// 2. Wurde bei Pflicht-Radios etwas ausgewählt?
|
||
// =============================
|
||
|
||
/**
|
||
* Prüft, ob der aktuelle Schritt gültig ist.
|
||
* Rückgabe:
|
||
* - true = alles okay
|
||
* - false = es gibt einen Fehler
|
||
*/
|
||
function validateCurrentStep() {
|
||
// Intro und Review müssen nicht validiert werden
|
||
if (currentStep === 0 || currentStep === lastStep) return true;
|
||
|
||
const fields = getStepFields(currentStep);
|
||
clearStepInvalidState(currentStep);
|
||
|
||
// Zuerst Radio-Gruppen prüfen
|
||
const radioCheck = validateRadioGroups(fields);
|
||
if (!radioCheck.isValid) {
|
||
setErrorMessage(radioCheck.message);
|
||
return false;
|
||
}
|
||
|
||
// Danach normale Pflichtfelder prüfen
|
||
const requiredCheck = validateRequiredFields(fields);
|
||
if (!requiredCheck.isValid) {
|
||
setErrorMessage(requiredCheck.message);
|
||
return false;
|
||
}
|
||
|
||
setErrorMessage("");
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Prüft alle Radio-Gruppen eines Schritts.
|
||
* Rückgabe:
|
||
* - Objekt mit isValid: true/false
|
||
* - bei Fehler zusätzlich eine passende Meldung
|
||
*/
|
||
function validateRadioGroups(fields) {
|
||
const names = [...new Set(fields.filter(f => f.type === "radio").map(f => f.name))];
|
||
|
||
for (const name of names) {
|
||
const group = Array.from(form.querySelectorAll(`input[name="${name}"]`));
|
||
const required = group.some(f => f.required);
|
||
const selected = group.some(f => f.checked);
|
||
|
||
if (required && !selected) {
|
||
markRadioGroupInvalid(group);
|
||
return {
|
||
isValid: false,
|
||
message: "Bitte wähle eine Option aus."
|
||
};
|
||
}
|
||
}
|
||
|
||
return { isValid: true };
|
||
}
|
||
|
||
/**
|
||
* Prüft alle Pflichtfelder ausser Radios und Checkboxen.
|
||
* Rückgabe:
|
||
* - Objekt mit isValid: true/false
|
||
* - bei Fehler zusätzlich eine passende Meldung
|
||
*/
|
||
function validateRequiredFields(fields) {
|
||
for (const field of fields) {
|
||
if (field.type === "radio" || field.type === "checkbox") continue;
|
||
|
||
if (!field.checkValidity()) {
|
||
markFieldInvalid(field);
|
||
return {
|
||
isValid: false,
|
||
message: "Bitte fülle alle Pflichtfelder aus."
|
||
};
|
||
}
|
||
}
|
||
|
||
return { isValid: true };
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 6: Navigation mit Zurück / Weiter
|
||
// Diese Funktionen bestimmen, was beim Klicken auf die Buttons passiert.
|
||
// =============================
|
||
|
||
/**
|
||
* Einen Schritt zurückgehen.
|
||
* Wenn der User im ersten Formularschritt ist, geht es zurück zum Intro.
|
||
*/
|
||
function handleBackClick() {
|
||
if (currentStep > 1) {
|
||
showStep(currentStep - 1);
|
||
} else {
|
||
showStep(0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Einen Schritt weitergehen.
|
||
* Vorher wird geprüft, ob der aktuelle Schritt gültig ist.
|
||
* Im letzten Schritt wird stattdessen das Formular abgeschickt.
|
||
*/
|
||
function handleNextClick() {
|
||
if (!validateCurrentStep()) return;
|
||
|
||
if (currentStep < lastStep) {
|
||
showStep(currentStep + 1);
|
||
} else {
|
||
form.requestSubmit();
|
||
}
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 7: Submit
|
||
// Aktuell ist das nur eine Demo.
|
||
// Später könnte hier ein API-Call oder Speichern in einer Datenbank passieren.
|
||
// =============================
|
||
|
||
/**
|
||
* Reagiert auf das Absenden des Formulars.
|
||
* preventDefault verhindert, dass die Seite neu lädt.
|
||
*/
|
||
function handleFormSubmit(event) {
|
||
event.preventDefault();
|
||
saveCurrentEvent();
|
||
steps.forEach(step => step.classList.remove("step--active"));
|
||
flowFooter.hidden = true;
|
||
submissionSuccess.hidden = false;
|
||
setErrorMessage("");
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 8: Counter-Felder (+ / -)
|
||
// Für Zahlenfelder wie z. B. Anzahl Gäste.
|
||
// =============================
|
||
|
||
/**
|
||
* Sucht alle Counter-Komponenten
|
||
* und verbindet die Plus-/Minus-Buttons mit den passenden Funktionen.
|
||
*/
|
||
function registerCounterHandlers() {
|
||
document.querySelectorAll("[data-counter]").forEach(counter => {
|
||
const input = counter.querySelector("input[type='number']");
|
||
const dec = counter.querySelector("[data-counter-action='decrease']");
|
||
const inc = counter.querySelector("[data-counter-action='increase']");
|
||
|
||
dec.addEventListener("click", () => updateCounterValue(input, -1));
|
||
inc.addEventListener("click", () => updateCounterValue(input, 1));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Erhöht oder verringert den Wert eines Zahlenfelds.
|
||
* Der Wert darf dabei nie kleiner als das definierte Minimum werden.
|
||
*/
|
||
function updateCounterValue(input, change) {
|
||
const min = Number(input.min || 1);
|
||
const currentValue = Number(input.value || min);
|
||
input.value = Math.max(min, currentValue + change);
|
||
}
|
||
|
||
/**
|
||
* Macht aus "-"+Enter im Menüfeld eine einfache Bullet-Liste.
|
||
*/
|
||
function registerMenuBulletHandler() {
|
||
const menuField = document.getElementById("menuDescription");
|
||
|
||
if (!menuField) return;
|
||
|
||
menuField.addEventListener("keydown", event => {
|
||
if (event.key !== "Enter") return;
|
||
|
||
const { selectionStart, selectionEnd, value } = menuField;
|
||
const lineStart = value.lastIndexOf("\n", selectionStart - 1) + 1;
|
||
const lineEnd = value.indexOf("\n", selectionStart);
|
||
const currentLineEnd = lineEnd === -1 ? value.length : lineEnd;
|
||
const currentLine = value.slice(lineStart, currentLineEnd);
|
||
const trimmedLine = currentLine.trim();
|
||
|
||
if (trimmedLine !== "-" && !currentLine.startsWith("• ")) return;
|
||
|
||
event.preventDefault();
|
||
|
||
const isEmptyBullet = currentLine.trim() === "•";
|
||
|
||
if (isEmptyBullet) {
|
||
const beforeLine = value.slice(0, lineStart);
|
||
const afterLine = value.slice(currentLineEnd);
|
||
const separator = beforeLine.endsWith("\n") || afterLine.startsWith("\n") ? "" : "\n";
|
||
const nextValue = `${beforeLine}${separator}${afterLine}`.replace(/\n{3,}/g, "\n\n");
|
||
|
||
menuField.value = nextValue;
|
||
const caretPosition = Math.min(lineStart, nextValue.length);
|
||
menuField.setSelectionRange(caretPosition, caretPosition);
|
||
menuField.dispatchEvent(new Event("input", { bubbles: true }));
|
||
return;
|
||
}
|
||
|
||
const bulletLine = currentLine.startsWith("• ") ? currentLine : currentLine.replace("-", "•");
|
||
const updatedLine = bulletLine.startsWith("• ") ? bulletLine : `• ${trimmedLine.slice(1).trimStart()}`;
|
||
const beforeLine = value.slice(0, lineStart);
|
||
const afterLine = value.slice(currentLineEnd);
|
||
const nextValue = `${beforeLine}${updatedLine}\n• ${afterLine}`;
|
||
const caretPosition = beforeLine.length + updatedLine.length + 3;
|
||
|
||
menuField.value = nextValue;
|
||
menuField.setSelectionRange(caretPosition, caretPosition);
|
||
menuField.dispatchEvent(new Event("input", { bubbles: true }));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Springt aus der Review zurück zum passenden Schritt
|
||
* und fokussiert das gewünschte Feld für direktes Weiterbearbeiten.
|
||
*/
|
||
function registerReviewEditHandlers() {
|
||
document.querySelectorAll(".review-item[data-edit-step]").forEach(item => {
|
||
const activateEdit = () => {
|
||
const stepIndex = Number(item.dataset.editStep);
|
||
const fieldName = item.dataset.editField;
|
||
|
||
showStep(stepIndex);
|
||
focusFieldByName(fieldName);
|
||
};
|
||
|
||
item.addEventListener("click", activateEdit);
|
||
item.addEventListener("keydown", event => {
|
||
if (event.key === "Enter" || event.key === " ") {
|
||
event.preventDefault();
|
||
activateEdit();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Entfernt Fehlermarkierungen, sobald der User ein Feld korrigiert.
|
||
*/
|
||
function registerValidationFeedbackHandlers() {
|
||
form.querySelectorAll("input, textarea, select").forEach(field => {
|
||
const clearInvalidState = () => {
|
||
field.classList.remove("field-invalid");
|
||
|
||
if (field.type === "radio") {
|
||
const group = Array.from(form.querySelectorAll(`input[name="${field.name}"]`));
|
||
const hasSelection = group.some(item => item.checked);
|
||
|
||
if (hasSelection) {
|
||
group.forEach(item => {
|
||
const card = item.closest(".option-card");
|
||
|
||
if (card) {
|
||
card.classList.remove("option-card--invalid");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
field.addEventListener("input", clearInvalidState);
|
||
field.addEventListener("change", clearInvalidState);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Setzt den Fokus auf ein bestimmtes Feld oder die erste Option einer Radio-Gruppe.
|
||
*/
|
||
function focusFieldByName(fieldName) {
|
||
const field = form.elements[fieldName];
|
||
|
||
if (!field) return;
|
||
|
||
const focusTarget = field instanceof RadioNodeList ? field[0] : field;
|
||
|
||
if (focusTarget && typeof focusTarget.focus === "function") {
|
||
window.setTimeout(() => {
|
||
focusTarget.focus();
|
||
}, 150);
|
||
}
|
||
}
|
||
|
||
|
||
// =============================
|
||
// STEP 9: Alles starten
|
||
// Hier werden alle Event Listener registriert
|
||
// und der Flow startet mit dem Intro-Schritt.
|
||
// =============================
|
||
|
||
/**
|
||
* Initialisiert den kompletten Event-Erstellungs-Flow.
|
||
* Diese Funktion wird einmal beim Laden der Seite aufgerufen.
|
||
*/
|
||
function initEventCreationFlow() {
|
||
// Buttons, die den Flow starten
|
||
document.querySelectorAll("[data-start-flow]").forEach(btn => {
|
||
btn.addEventListener("click", () => showStep(1));
|
||
});
|
||
|
||
// Navigation
|
||
backButton.addEventListener("click", handleBackClick);
|
||
nextButton.addEventListener("click", handleNextClick);
|
||
|
||
// Formular absenden
|
||
form.addEventListener("submit", handleFormSubmit);
|
||
|
||
// Counter aktivieren
|
||
registerCounterHandlers();
|
||
registerMenuBulletHandler();
|
||
registerValidationFeedbackHandlers();
|
||
registerReviewEditHandlers();
|
||
|
||
// Startzustand: Intro anzeigen
|
||
submissionSuccess.hidden = true;
|
||
showStep(0);
|
||
}
|
||
|
||
// Startpunkt des Skripts
|
||
initEventCreationFlow();
|