/* ===================================================================== wd-anfrage.js — Steuerung des mehrstufigen Anfrageformulars (Masterplan S.10 "Projektanfrage ohne Informationsverlust") Aufbau: 4 Eingabeschritte + Zusammenfassung + Dankeseite. Leitgedanken: - Nichts geht verloren. Jede Eingabe landet sofort im Browserspeicher; ein geschlossener Tab, ein leerer Akku oder ein versehentliches Zurück kostet keine Arbeit. - Geprüft wird beim Weitergehen, nicht beim Tippen. Wer noch mitten im Wort ist, soll nicht angemeckert werden. - Die Zusammenfassung zeigt, was WIRKLICH abgeschickt wird — aus den Feldern gelesen, nicht aus dem, was das Skript zu wissen glaubt. ===================================================================== */ (function () { "use strict"; var SPEICHER = "wd-anfrage-entwurf"; var LETZTE_SEITE_EINGABE = 4; // danach kommt die Zusammenfassung var SEITE_ZUSAMMENFASSUNG = 5; var SEITE_DANKE = 6; var form, seiten, zaehler, marken, meldung, knopfLeiste, btnZurueck, btnWeiter; var aktuelleSeite = 1; var geoeffnetAm = Date.now(); /* ------------------------------------------------------------------ Werte einsammeln. Bewusst direkt aus dem Formular statt aus einem nebenher gepflegten Objekt: so kann die Anzeige nie etwas anderes behaupten als das, was tatsächlich in den Feldern steht. ------------------------------------------------------------------ */ function werte() { var d = new FormData(form); var funktionen = d.getAll("funktionen"); /* Kategoriespezifische Felder tragen das Präfix "d_" und wandern gesammelt in ein Unterobjekt. Neue Folgefragen brauchen dadurch weder hier noch in der Datenbank eine Änderung. */ var details = {}; form.querySelectorAll("[name^='d_']").forEach(function (el) { /* Nur Felder mitnehmen, die gerade sichtbar sind — sonst schickt jemand, der von "Shop" auf "Onepager" zurückwechselt, immer noch seine alten Shop-Antworten mit. */ if (el.closest("[hidden]")) return; var wert = (el.value || "").trim(); if (wert) details[el.name.slice(2)] = wert; }); var vorhanden = {}; form.querySelectorAll("[name^='v_']").forEach(function (el) { if (el.checked) vorhanden[el.name.slice(2)] = el.value; }); return { paket: d.get("paket") || "", ziel: (d.get("ziel") || "").trim(), zielgruppe: (d.get("zielgruppe") || "").trim(), seiten: (d.get("seiten") || "").trim(), funktionen: funktionen, details: details, hatTexte: vorhanden.texte || "", hatBilder: vorhanden.bilder || "", hatLogo: vorhanden.logo || "", hatDomain: vorhanden.domain || "", budget: d.get("budget") || "", termin: (d.get("termin") || "").trim(), name: (d.get("name") || "").trim(), email: (d.get("email") || "").trim(), telefon: (d.get("telefon") || "").trim(), kontaktweg: d.get("kontaktweg") || "email", bemerkung: (d.get("bemerkung") || "").trim(), datenschutz: !!d.get("datenschutz"), website: (d.get("website") || "").trim(), // Honigtopf }; } /* ------------------------------------------------------------------ Zwischenspeicherung (Masterplan S.10: "Zwischenspeicherung verhindert Datenverlust") ------------------------------------------------------------------ */ function speichern() { try { var w = werte(); delete w.website; // Honigtopf nie speichern sessionStorageSchreiben(JSON.stringify({ stand: w, seite: aktuelleSeite, zeit: Date.now() })); } catch (e) { /* Speicher voll oder gesperrt — kein Grund abzubrechen */ } } function sessionStorageSchreiben(text) { /* localStorage, NICHT sessionStorage: der Entwurf soll auch morgen noch da sein. Das ist der ganze Zweck. */ try { localStorage.setItem(SPEICHER, text); } catch (e) { /* egal */ } } function gespeichertesLesen() { try { var roh = localStorage.getItem(SPEICHER); if (!roh) return null; var daten = JSON.parse(roh); /* Entwürfe verfallen nach 30 Tagen. Ein halbes Jahr alter Entwurf verwirrt mehr, als er hilft. */ if (!daten.zeit || Date.now() - daten.zeit > 30 * 24 * 3600 * 1000) { localStorage.removeItem(SPEICHER); return null; } return daten; } catch (e) { return null; } } function entwurfLoeschen() { try { localStorage.removeItem(SPEICHER); } catch (e) { /* egal */ } } function wiederherstellen(daten) { var w = daten.stand || {}; function setz(name, wert) { var el = form.querySelector("[name='" + name + "']"); if (el && typeof wert === "string") el.value = wert; } setz("ziel", w.ziel); setz("zielgruppe", w.zielgruppe); setz("seiten", w.seiten); setz("termin", w.termin); setz("name", w.name); setz("email", w.email); setz("telefon", w.telefon); setz("bemerkung", w.bemerkung); var budget = form.querySelector("[name='budget']"); if (budget && w.budget) budget.value = w.budget; if (w.paket) { var p = form.querySelector("[name='paket'][value='" + w.paket + "']"); if (p) p.checked = true; } if (w.kontaktweg) { var k = form.querySelector("[name='kontaktweg'][value='" + w.kontaktweg + "']"); if (k) k.checked = true; } (w.funktionen || []).forEach(function (f) { var el = form.querySelector("[name='funktionen'][value='" + f + "']"); if (el) el.checked = true; }); Object.keys(w.details || {}).forEach(function (k) { var el = form.querySelector("[name='d_" + k + "']"); if (el) el.value = w.details[k]; }); [["texte", w.hatTexte], ["bilder", w.hatBilder], ["logo", w.hatLogo], ["domain", w.hatDomain]] .forEach(function (paar) { if (!paar[1]) return; var el = form.querySelector("[name='v_" + paar[0] + "'][value='" + paar[1] + "']"); if (el) el.checked = true; }); if (w.datenschutz) { var ds = form.querySelector("[name='datenschutz']"); if (ds) ds.checked = true; } } /* ------------------------------------------------------------------ Tabelle "Was hast du schon?" — per JS gebaut, damit die vier Zeilen nicht viermal fast gleich im HTML stehen. ------------------------------------------------------------------ */ function baueVorhandenTabelle() { var koerper = document.getElementById("af-vorhanden"); if (!koerper) return; var zeilen = [ ["texte", "af3_texte"], ["bilder", "af3_bilder"], ["logo", "af3_logo"], ["domain", "af3_domain"], ]; var antworten = [["ja", "af3_ja"], ["teilweise", "af3_teilweise"], ["nein", "af3_nein"]]; koerper.innerHTML = zeilen.map(function (z) { var knoepfe = antworten.map(function (a) { return '"; }).join(""); return '' + window.WD.t(z[1]) + "" + knoepfe + ""; }).join(""); } /* ------------------------------------------------------------------ Prüfung je Schritt ------------------------------------------------------------------ */ function fehlerSetzen(feldId, text) { var feld = document.getElementById(feldId); if (!feld) return; feld.classList.toggle("wd-feld--fehler", !!text); var p = feld.querySelector(".wd-feld-fehler"); if (p) p.textContent = text || ""; } function fehlerLeeren() { form.querySelectorAll(".wd-feld--fehler").forEach(function (f) { f.classList.remove("wd-feld--fehler"); var p = f.querySelector(".wd-feld-fehler"); if (p) p.textContent = ""; }); } /* Dieselbe absichtlich einfache Prüfung wie auf dem Server: Die einzige zuverlässige Kontrolle ist ohnehin, ob die Antwortmail ankommt. Eine strenge Regel lehnt vor allem gültige Adressen ab. */ function istEmail(s) { var t = String(s || "").trim().toLowerCase(); var teile = t.split("@"); return teile.length === 2 && !!teile[0] && teile[1].indexOf(".") > 0 && !/\s/.test(t); } function pruefeSeite(nr) { fehlerLeeren(); var w = werte(); var fehler = []; if (nr === 1) { if (!w.paket) { fehlerSetzen("feld-paket", window.WD.t("af_fehler_paket")); fehler.push("feld-paket"); } if (!w.ziel) { fehlerSetzen("feld-ziel", window.WD.t("af_fehler_ziel")); fehler.push("feld-ziel"); } } if (nr === 4) { if (!w.name) { fehlerSetzen("feld-name", window.WD.t("af_fehler_name")); fehler.push("feld-name"); } if (!istEmail(w.email)) { fehlerSetzen("feld-email", window.WD.t("af_fehler_email")); fehler.push("feld-email"); } /* Wer telefonisch kontaktiert werden möchte, muss eine Nummer angeben — sonst kann der Wunsch gar nicht erfüllt werden. */ if (w.kontaktweg === "telefon" && !w.telefon) { fehlerSetzen("feld-telefon", window.WD.t("af_fehler_telefon")); fehler.push("feld-telefon"); } if (!w.datenschutz) { fehlerSetzen("feld-datenschutz", window.WD.t("af_fehler_datenschutz")); fehler.push("feld-datenschutz"); } } if (fehler.length) { window.WD.meldung(meldung, window.WD.t("af_fehler_schritt"), "fehler"); var erstes = document.getElementById(fehler[0]); if (erstes) { erstes.scrollIntoView({ behavior: "smooth", block: "center" }); var eingabe = erstes.querySelector("input, textarea, select"); if (eingabe) setTimeout(function () { eingabe.focus({ preventScroll: true }); }, 300); } return false; } window.WD.meldungAus(meldung); return true; } /* ------------------------------------------------------------------ Navigation ------------------------------------------------------------------ */ function zeigeSeite(nr) { aktuelleSeite = nr; seiten.forEach(function (s) { s.classList.toggle("af-sichtbar", Number(s.dataset.seite) === nr); }); marken.forEach(function (m) { var i = Number(m.dataset.marke); m.classList.toggle("af-aktiv", i === nr); m.classList.toggle("af-erledigt", i < nr); }); var fortschritt = document.getElementById("af-fortschritt"); var hinweis = document.getElementById("af-speicherhinweis"); if (nr === SEITE_DANKE) { knopfLeiste.hidden = true; if (fortschritt) fortschritt.hidden = true; if (hinweis) hinweis.hidden = true; } else { knopfLeiste.hidden = false; if (fortschritt) fortschritt.hidden = false; if (hinweis) hinweis.hidden = false; btnZurueck.hidden = nr === 1; btnWeiter.textContent = nr === SEITE_ZUSAMMENFASSUNG ? window.WD.t("af_senden") : nr === LETZTE_SEITE_EINGABE ? window.WD.t("af_pruefen") : window.WD.t("af_weiter"); zaehler.textContent = window.WD.t("af_schritt") + " " + Math.min(nr, 4) + " " + window.WD.t("af_von") + " 4"; } /* Nach oben, aber nur bis zum Formular — ein Sprung ganz nach oben zwingt sonst bei jedem Schritt zum Zurückscrollen. */ var rahmen = document.getElementById("af-rahmen"); if (rahmen) { var y = rahmen.getBoundingClientRect().top + window.scrollY - 80; window.scrollTo({ top: Math.max(0, y), behavior: "smooth" }); } /* Auf der Dankeseite NICHT mehr speichern. Sonst passiert Folgendes: absenden() loescht den Entwurf, ruft dann zeigeSeite(DANKE) auf — und diese Zeile legt ihn sofort wieder an. Beim naechsten Besuch stuenden die Daten einer bereits gesendeten Anfrage wieder im Formular, samt "Wir haben deine letzte Eingabe wiederhergestellt". Genau so im Selbsttest aufgefallen. */ if (nr !== SEITE_DANKE) speichern(); } function folgefragenAktualisieren() { var w = werte(); var shop = w.paket === "shop" || w.funktionen.indexOf("shop") >= 0; var vw = w.paket === "verwaltung" || w.funktionen.indexOf("login") >= 0; var eShop = document.getElementById("af-folge-shop"); var eVw = document.getElementById("af-folge-verwaltung"); if (eShop) eShop.hidden = !shop; if (eVw) eVw.hidden = !vw; } /* ------------------------------------------------------------------ Zusammenfassung ------------------------------------------------------------------ */ function beschriftung(schluessel, roh) { if (!roh) return null; var t = window.WD.t(schluessel); return t === schluessel ? roh : t; } function baueZusammenfassung() { var w = werte(); var ziel = document.getElementById("af-zusammenfassung"); var sicher = window.WD.sicher; var leer = window.WD.t("af_zus_leer"); function zeile(was, wert) { var istLeer = !wert; return '
' + sicher(was) + "" + '' + sicher(istLeer ? leer : wert) + "
"; } function block(titel, schrittNr, zeilen) { return '

' + sicher(titel) + "

" + '" + "
" + zeilen.join("") + "
"; } var paketName = { onepager: "pk_onepager_h3", website: "pk_website_h3", shop: "pk_shop_h3", verwaltung: "pk_verwaltung_h3", betreuung: "betreuung_h3", unklar: "af1_unklar", }[w.paket]; var funktionsNamen = { kontaktformular: "af2_f_kontakt", galerie: "af2_f_galerie", termin: "af2_f_termin", shop: "af2_f_shop", login: "af2_f_login", blog: "af2_f_blog", sprachen: "af2_f_sprachen", karte: "af2_f_karte", newsletter: "af2_f_newsletter", app: "af2_f_app", }; var funktionenText = w.funktionen .map(function (f) { return beschriftung(funktionsNamen[f], f); }) .filter(Boolean) /* HTML-Entities aus dem Wörterbuch (&) in echte Zeichen — hier ist reiner Text gefragt, er wird gleich escaped. */ .map(function (s) { return s.replace(/&/g, "&"); }) .join(", "); var antwortNamen = { ja: "af3_ja", teilweise: "af3_teilweise", nein: "af3_nein" }; var budgetNamen = { "bis-500": "af3_budget_1", "500-1000": "af3_budget_2", "1000-2500": "af3_budget_3", "ueber-2500": "af3_budget_4", unklar: "af3_budget_5", }; var html = ""; html += block(window.WD.t("af_s1"), 1, [ zeile(window.WD.t("af1_paket_label"), beschriftung(paketName, w.paket)), zeile(window.WD.t("af1_ziel_label"), w.ziel), zeile(window.WD.t("af1_zielgruppe_label"), w.zielgruppe), ]); var zeilen2 = [ zeile(window.WD.t("af2_seiten_label"), w.seiten), zeile(window.WD.t("af2_funktionen_label"), funktionenText), ]; Object.keys(w.details).forEach(function (k) { zeilen2.push(zeile(k.replace(/_/g, " "), w.details[k])); }); html += block(window.WD.t("af_s2"), 2, zeilen2); html += block(window.WD.t("af_s3"), 3, [ zeile(window.WD.t("af3_texte"), beschriftung(antwortNamen[w.hatTexte], w.hatTexte)), zeile(window.WD.t("af3_bilder"), beschriftung(antwortNamen[w.hatBilder], w.hatBilder)), zeile(window.WD.t("af3_logo"), beschriftung(antwortNamen[w.hatLogo], w.hatLogo)), zeile(window.WD.t("af3_domain"), beschriftung(antwortNamen[w.hatDomain], w.hatDomain)), zeile(window.WD.t("af3_budget_label"), beschriftung(budgetNamen[w.budget], w.budget)), zeile(window.WD.t("af3_termin_label"), w.termin), ]); html += block(window.WD.t("af_s4"), 4, [ zeile(window.WD.t("af4_name"), w.name), zeile(window.WD.t("af4_email"), w.email), zeile(window.WD.t("af4_telefon"), w.telefon), zeile(window.WD.t("af4_weg"), window.WD.t(w.kontaktweg === "telefon" ? "af4_weg_telefon" : "af4_weg_email")), zeile(window.WD.t("af4_bemerkung"), w.bemerkung), ]); ziel.innerHTML = html; ziel.querySelectorAll("[data-zu]").forEach(function (b) { b.addEventListener("click", function () { zeigeSeite(Number(b.dataset.zu)); }); }); } /* ------------------------------------------------------------------ Absenden ------------------------------------------------------------------ */ async function absenden() { var w = werte(); window.WD.knopfSperren(btnWeiter, true, window.WD.t("af_sendet")); window.WD.meldungAus(meldung); try { var antwort = await window.WD.api("/webdesign/anfrage", { method: "POST", body: Object.assign({}, w, { sprache: window.WD.getSprache(), geoeffnetAm: geoeffnetAm, }), }); /* Erst NACH bestätigtem Eingang löschen. Bricht das Absenden ab, sind die Eingaben noch da und man kann es erneut versuchen. */ /* Der Server meldet bei verdächtigen Absendungen bewusst Erfolg, speichert aber nichts und liefert KEINE Nummer (Honigtopf oder verdächtig kurze Ausfüllzeit — siehe routes/webdesign-anfragen.js). Das ist gegenüber automatischen Skripten richtig so. Für einen echten Menschen, der zufällig sehr schnell war, wäre eine Dankeseite mit "—" als Nummer aber eine stille Lüge: er glaubt, die Anfrage sei unterwegs, und wartet auf eine Antwort, die nie kommt. Deshalb hier ohne Nummer ein ehrlicher Hinweis mit einem zweiten Weg. */ var nummerFeld = document.getElementById("af-nummer"); if (antwort.nummer) { entwurfLoeschen(); nummerFeld.textContent = antwort.nummer; } else { /* Entwurf ABSICHTLICH behalten — falls wirklich nichts ankam, soll die Arbeit nicht weg sein. */ nummerFeld.textContent = "—"; nummerFeld.style.fontSize = "1rem"; nummerFeld.style.letterSpacing = "normal"; nummerFeld.textContent = window.WD.t("af_danke_ohne_nummer"); } zeigeSeite(SEITE_DANKE); } catch (fehler) { /* Feldbezogene Fehler vom Server anzeigen — der Server ist die maßgebliche Prüfung, nicht der Browser. */ var felder = fehler && fehler.daten && fehler.daten.felder; if (felder) { Object.keys(felder).forEach(function (f) { fehlerSetzen("feld-" + f, felder[f]); }); } window.WD.meldung(meldung, fehler.message || window.WD.t("fehler_allg"), "fehler"); window.WD.knopfSperren(btnWeiter, false); } } /* ------------------------------------------------------------------ Start ------------------------------------------------------------------ */ function start() { form = document.getElementById("af-form"); if (!form) return; seiten = Array.prototype.slice.call(form.querySelectorAll(".af-seite")); marken = Array.prototype.slice.call(document.querySelectorAll(".af-schritt-marke")); zaehler = document.getElementById("af-zaehler"); meldung = document.getElementById("af-meldung"); knopfLeiste = document.getElementById("af-knopfleiste"); btnZurueck = document.getElementById("af-zurueck"); btnWeiter = document.getElementById("af-weiter"); baueVorhandenTabelle(); /* Paket aus der Adresszeile vorauswählen — die Leistungsseite verlinkt mit ?paket=shop. Spart einen Klick und zeigt, dass der Weg mitgedacht ist. */ var params = new URLSearchParams(location.search); var paket = params.get("paket"); if (paket) { var el = form.querySelector("[name='paket'][value='" + paket.replace(/[^a-z]/g, "") + "']"); if (el) el.checked = true; } var entwurf = gespeichertesLesen(); if (entwurf) { wiederherstellen(entwurf); var box = document.getElementById("af-wieder"); if (box) box.hidden = false; } document.getElementById("af-verwerfen").addEventListener("click", function () { entwurfLoeschen(); location.reload(); }); folgefragenAktualisieren(); btnWeiter.addEventListener("click", function () { if (aktuelleSeite === SEITE_ZUSAMMENFASSUNG) return absenden(); if (!pruefeSeite(aktuelleSeite)) return; if (aktuelleSeite === LETZTE_SEITE_EINGABE) { baueZusammenfassung(); zeigeSeite(SEITE_ZUSAMMENFASSUNG); return; } zeigeSeite(aktuelleSeite + 1); }); btnZurueck.addEventListener("click", function () { if (aktuelleSeite > 1) zeigeSeite(aktuelleSeite - 1); }); /* Eingaben laufend sichern und Folgefragen nachziehen. "input" statt "change", damit auch beim Tippen gespeichert wird. */ form.addEventListener("input", function () { folgefragenAktualisieren(); speichern(); }); form.addEventListener("change", function () { folgefragenAktualisieren(); speichern(); }); /* Enter im Formular soll weitergehen, nicht absenden — sonst ist die Anfrage nach dem ersten Feld unterwegs. */ form.addEventListener("submit", function (e) { e.preventDefault(); btnWeiter.click(); }); /* Telefon wird bei "Per Telefon" zur Pflicht — das Wörtchen "optional" muss dann verschwinden, sonst ist die spätere Fehlermeldung unfair. */ form.querySelectorAll("[name='kontaktweg']").forEach(function (r) { r.addEventListener("change", function () { var opt = document.getElementById("af-telefon-opt"); if (opt) opt.hidden = r.value === "telefon" && r.checked; }); }); /* Nach einem Sprachwechsel Knopfbeschriftung, Zähler und die per JS gebauten Teile neu setzen. */ document.addEventListener("wd-sprache-geaendert", function () { baueVorhandenTabelle(); wiederherstellen({ stand: werte() }); // Auswahl der Tabelle retten if (aktuelleSeite === SEITE_ZUSAMMENFASSUNG) baueZusammenfassung(); zeigeSeite(aktuelleSeite); }); zeigeSeite(entwurf && entwurf.seite && entwurf.seite <= LETZTE_SEITE_EINGABE ? entwurf.seite : 1); } if (window.WD) start(); else document.addEventListener("wd-bereit", start); })();