/*! WEBklar Webshop: Einbau-Skript fuer Kundenwebsites. *
Ausweichtext
* data-ansicht="widerruf" oeffnet gleich die Widerrufsfunktion ("Vertrag widerrufen"); die Seitenadresse kennt ?ansicht=widerruf und ?ansicht=agb. * Keine Abhaengigkeiten, keine Cookies, keine Drittanfragen. Einziger Browser-Speicher: der Warenkorb (nur Artikel-IDs und Mengen) in der * sessionStorage des Besuchers, solange die Seite im Tab geoeffnet ist (technisch erforderlich, kein Tracking). Alle Texte der Haendler nur ueber * textContent. Das Widget rechnet nie: Preise, Summen, Versand, Steuerhinweise und Datumstexte kommen als fertige Texte vom Server. */ (function () { 'use strict' var SELF = document.currentScript var STYLE_ID = 'wks-style' var API_PFAD = '/api/public/shop/v1/' var CONFIG_ALTER_MAX_MS = 100 * 60 * 1000 // vor dem Ablauf des Formular-Tokens (2 h) neu laden var TIMEOUT_MS = 20000 var FORM_MIN_VORGABE_S = 3 // so lange muss ein Formular-Token mindestens alt sein (wie der Server) var FORM_PUFFER_MS = 500 // Sicherheitszuschlag zu FORM_MIN_VORGABE_S (Laufzeit der Anfrage) var MAX_MENGE = 99 var MAX_POSITIONEN = 50 var STATUS_VERSUCHE = 5 // Abrufe des Bestellstatus nach der Rueckkehr von Stripe, solange die Bestellung "offen" ist var STATUS_ABSTAND_MS = 2000 var PREFIX = 'wks' + Math.random().toString(36).slice(2, 6) + '-' var zaehler = 0 var EMAIL_FORM = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/ var ID_FORM = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/ var TOKEN_FORM = /^[A-Za-z0-9._~-]{8,300}$/ var LAENDER = { DE: 'Deutschland', AT: 'Österreich' } var rueckkehr = null // Rueckkehr von Stripe (wk_bestellung/wk_t oder wk_abbruch): der erste Container, der startet, verbraucht sie var queryAnsicht = '' var CSS = [ '@layer wks{', '.wks-root{--wks-i-akzent:var(--wks-akzent,var(--wks-vorgabe-akzent,#1d4ed8));--wks-i-akzent-text:var(--wks-akzent-text,var(--wks-vorgabe-akzent-text,#fff));', '--wks-i-bg:var(--wks-hintergrund,#fff);--wks-i-text:var(--wks-text,#1f2937);--wks-i-rand:var(--wks-rand,#858f9e);--wks-i-radius:var(--wks-radius,10px);', '--wks-i-weich:#f3f4f6;--wks-i-fehler:#b91c1c;--wks-i-schrift:var(--wks-schrift,inherit);--wks-i-titel:var(--wks-schrift-titel,var(--wks-schrift,inherit))}', '@supports (color:color-mix(in srgb,red,blue)){.wks-root{--wks-i-weich:color-mix(in srgb,var(--wks-i-text) 6%,var(--wks-i-bg))}}', '.wks-root{container:wks/inline-size;display:block;position:relative;box-sizing:border-box;padding:20px;border:1px solid #c9ced6;border-radius:var(--wks-i-radius);', 'background:var(--wks-i-bg);color:var(--wks-i-text);font-family:var(--wks-i-schrift);font-size:16px;line-height:1.5;text-align:left}', '.wks-root *,.wks-root *::before,.wks-root *::after{box-sizing:border-box}', '.wks-root [hidden]{display:none!important}', '.wks-vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}', '.wks-kopfzeile{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem 1rem;margin:0 0 .25rem}', '.wks-shopname{font-family:var(--wks-i-titel);font-size:1.5rem;line-height:1.2;font-weight:700;margin:0;color:inherit}', '.wks-lieferinfo{margin:0 0 .75rem;font-size:.9rem;opacity:.9}', '.wks-titel{font-family:var(--wks-i-titel);font-size:1.3rem;line-height:1.25;font-weight:700;margin:.5rem 0 .75rem;color:inherit}', '.wks-titel:focus{outline:none}.wks-titel:focus-visible{outline:3px solid var(--wks-i-text);outline-offset:3px}', '.wks-untertitel{font-size:1.05rem;font-weight:700;margin:1rem 0 .35rem;color:inherit}', '.wks-root p{margin:0 0 .75rem}', '.wks-laden{opacity:.8}', '.wks-btn{font:inherit;font-weight:600;min-height:44px;padding:.55rem 1.25rem;border-radius:var(--wks-i-radius);border:2px solid var(--wks-i-akzent);background:var(--wks-i-akzent);color:var(--wks-i-akzent-text);cursor:pointer;line-height:1.25}', '.wks-btn:hover{filter:brightness(1.08)}', '.wks-btn-sek{background:transparent;color:inherit;border-color:var(--wks-i-rand)}', '.wks-btn[disabled]{opacity:.6;cursor:default}', '.wks-link{font:inherit;color:inherit;background:none;border:0;padding:.2rem 0;min-height:24px;text-decoration:underline;cursor:pointer;text-align:left}', '.wks-nav{display:flex;flex-wrap:wrap;gap:.75rem;justify-content:space-between;align-items:center;margin-top:1.25rem}', '.wks-nav>:only-child{margin-left:auto}', '.wks-root :focus-visible{outline:3px solid var(--wks-i-text);outline-offset:2px}', '.wks-chips{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1rem}', '.wks-chip{font:inherit;min-height:44px;padding:.4rem 1rem;border-radius:999px;border:1px solid var(--wks-i-rand);background:var(--wks-i-bg);color:inherit;cursor:pointer}', '.wks-chip[aria-pressed="true"]{background:var(--wks-i-akzent);color:var(--wks-i-akzent-text);border-color:var(--wks-i-akzent);font-weight:600}', '.wks-raster{list-style:none;margin:0 0 1rem;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(11rem,1fr));gap:1rem}', '.wks-raster>li{margin:0;padding:0}', '.wks-karte{position:relative;display:flex;flex-direction:column;gap:.3rem;height:100%;border:1px solid var(--wks-i-rand);border-radius:var(--wks-i-radius);padding:.6rem;background:var(--wks-i-bg)}', '.wks-karte:hover{border-color:var(--wks-i-akzent)}', '.wks-karte:focus-within{outline:3px solid var(--wks-i-text);outline-offset:2px}', '.wks-karte-bild,.wks-karte-leer{display:block;width:100%;aspect-ratio:1/1;object-fit:cover;border-radius:calc(var(--wks-i-radius) * .7);background:var(--wks-i-weich)}', '.wks-karte-name{margin:0;font-size:1rem;line-height:1.3}', '.wks-karte-knopf{font:inherit;font-weight:700;text-align:left;background:none;border:0;padding:0;color:inherit;cursor:pointer}', '.wks-karte-knopf::after{content:"";position:absolute;inset:0}', '.wks-karte-knopf:focus-visible{outline:none}', '.wks-karte-text{font-size:.9rem;margin:0}', '.wks-preis{font-weight:700;margin:0}', '.wks-grundpreis,.wks-klein{font-size:.85rem;opacity:.85;margin:0}', '.wks-aus{font-size:.85rem;font-weight:600;color:var(--wks-i-fehler);margin:0}', '.wks-preis-versand{margin:.1rem 0 .35rem;font-size:.85rem}', '.wks-hinweiszeile{font-size:.85rem;margin:0 0 .75rem;opacity:.9}', '.wks-detail{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1.25rem;margin:0 0 1rem}', '.wks-galerie-gross{display:block;width:100%;aspect-ratio:1/1;object-fit:contain;border:1px solid var(--wks-i-rand);border-radius:var(--wks-i-radius);background:var(--wks-i-weich)}', '.wks-daumen{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.5rem}', '.wks-daumen button{width:4rem;height:4rem;min-width:44px;min-height:44px;padding:0;border:2px solid var(--wks-i-rand);border-radius:calc(var(--wks-i-radius) * .7);background:var(--wks-i-bg);cursor:pointer;overflow:hidden}', '.wks-daumen button[aria-pressed="true"]{border-color:var(--wks-i-akzent)}', '.wks-daumen img{display:block;width:100%;height:100%;object-fit:cover}', '.wks-beschreibung p,.wks-text p{white-space:pre-line}', '.wks-form{margin:0}', '.wks-feld{margin:0 0 1rem;min-width:0}', 'fieldset.wks-feld{border:1px solid var(--wks-i-rand);border-radius:var(--wks-i-radius);padding:.5rem 1rem .75rem}', '.wks-feld legend{font-weight:600;padding:0 .25rem}', '.wks-label{display:block;font-weight:600;margin-bottom:.25rem}', '.wks-stern{color:var(--wks-i-fehler)}', '.wks-hinweis{font-size:.875rem;margin:0 0 .35rem;opacity:.9}', '.wks-eingabe{display:block;width:100%;min-height:44px;font:inherit;color:inherit;background:var(--wks-i-bg);border:1px solid var(--wks-i-rand);border-radius:calc(var(--wks-i-radius) * .7);padding:.5rem .7rem}', '.wks-eingabe-menge{width:6rem}', 'textarea.wks-eingabe{min-height:6rem;resize:vertical}', '.wks-eingabe[aria-invalid="true"]{border-color:var(--wks-i-fehler);border-width:2px}', '.wks-feldfehler,.wks-fehlertext{color:var(--wks-i-fehler);font-weight:600;font-size:.9rem;margin:.3rem 0 0}', '.wks-fehlertext{margin:0 0 .75rem}', '.wks-meldung{margin:0 0 .75rem;padding:.6rem .8rem;border-left:4px solid var(--wks-i-akzent);background:var(--wks-i-weich);font-weight:600}', '.wks-radio{display:flex;gap:.65rem;align-items:flex-start;margin:0 0 .5rem;min-height:44px}', '.wks-radio input{flex:none;width:1.4rem;height:1.4rem;margin:.35rem 0 0;accent-color:var(--wks-i-akzent)}', '.wks-radio label{cursor:pointer;padding-top:.2rem}', '.wks-zeilen{list-style:none;margin:0 0 1rem;padding:0;display:grid;gap:.6rem}', '.wks-zeilen>li{margin:0;padding:.7rem .9rem;border:1px solid var(--wks-i-rand);border-radius:var(--wks-i-radius);display:flex;flex-wrap:wrap;gap:.5rem 1rem;align-items:center;justify-content:space-between}', '.wks-zeile-name{font-weight:700;margin:0}', '.wks-zeile-info{flex:1 1 12rem;min-width:0}', '.wks-zeile-menge{display:flex;align-items:center;gap:.5rem}', '.wks-zeile-menge label{font-size:.9rem}', '.wks-pflicht{margin:1.25rem 0;padding:.9rem 1rem;border:2px solid var(--wks-i-akzent);border-radius:var(--wks-i-radius);background:var(--wks-i-bg)}', '.wks-pflicht>:last-child{margin-bottom:0}', '.wks-pflicht h4{font-size:1rem;margin:.9rem 0 .25rem}', '.wks-pflicht h4:first-child{margin-top:0}', '.wks-pos{list-style:none;margin:0 0 .75rem;padding:0;display:grid;gap:.5rem}', '.wks-pos>li{display:flex;flex-wrap:wrap;justify-content:space-between;gap:.25rem 1rem;margin:0}', '.wks-pos-text{flex:1 1 12rem;min-width:0;overflow-wrap:anywhere}', '.wks-summen{margin:0 0 .75rem;display:grid;grid-template-columns:1fr max-content;gap:.25rem 1rem}', '.wks-summen dt{margin:0}.wks-summen dd{margin:0;text-align:right}', '.wks-summen .wks-gesamt{font-weight:700;font-size:1.1rem;border-top:1px solid var(--wks-i-rand);padding-top:.35rem}', '.wks-zusammen{margin:0 0 1rem;display:grid;grid-template-columns:max-content 1fr;gap:.35rem 1rem}', '.wks-zusammen dt{font-weight:600}.wks-zusammen dd{margin:0;overflow-wrap:anywhere;white-space:pre-line}', '.wks-gewaehr{margin:1.25rem 0;padding:.9rem 1rem;border:2px solid var(--wks-i-akzent);border-radius:var(--wks-i-radius);background:var(--wks-i-weich)}', '.wks-gewaehr h4{font-size:1rem;margin:0 0 .6rem}', '.wks-gewaehr img{display:block;width:100%;max-width:420px;height:auto;margin:0 0 .6rem;border:1px solid var(--wks-i-rand);border-radius:calc(var(--wks-i-radius) * .7);background:#fff}', '.wks-gewaehr>:last-child{margin-bottom:0}', '.wks-agb-hinweis{margin:0 0 .75rem}', 'dialog.wks-dialog.wks-dialog-gross{width:min(64rem,calc(100vw - 1.5rem))}', '.wks-dialog-bild{display:block;max-width:100%;height:auto;margin:0 auto 1rem;background:#fff}', '.wks-links{display:flex;flex-wrap:wrap;gap:.25rem 1.25rem;margin:0 0 .75rem}', '.wks-box{margin:0 0 1rem;padding:.75rem 1rem;border-radius:var(--wks-i-radius);background:var(--wks-i-weich)}', '.wks-box h4{font-size:1rem;margin:0 0 .25rem}', '.wks-box p{white-space:pre-line}', '.wks-fuss{margin-top:1.5rem;padding-top:.75rem;border-top:1px solid #c9ced6;font-size:.875rem}', '.wks-fuss-links{display:flex;flex-wrap:wrap;align-items:baseline;gap:.35rem 1.25rem;margin:0 0 .5rem}', '.wks-fuss a{display:inline-block;padding:.2rem 0;min-height:24px;color:inherit}', '.wks-anbieter{margin:0 0 .35rem;opacity:.85;overflow-wrap:anywhere}', '.wks-powered{margin:0;opacity:.75;font-size:.8rem}', '.wks-ergebnis-text{font-size:1.05rem}', 'dialog.wks-dialog{position:fixed;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;padding:0;border:0;overflow:auto;width:min(40rem,calc(100vw - 1.5rem));max-height:calc(100vh - 1.5rem);max-height:calc(100dvh - 1.5rem);border-radius:var(--wks-radius,10px);background:var(--wks-hintergrund,#fff);color:var(--wks-text,#1f2937);box-shadow:0 20px 50px rgba(0,0,0,.35)}', 'dialog.wks-dialog::backdrop{background:rgba(15,23,42,.55)}', '.wks-dialog-innen{padding:20px;font-family:var(--wks-schrift,inherit);font-size:16px;line-height:1.5;text-align:left}', '.wks-dialog-innen h3{font-size:1.2rem;margin:0 3rem .75rem 0}', '.wks-dialog-text{font-size:.95rem}', '.wks-dialog-text p{white-space:pre-line;margin:0 0 .75rem}', '.wks-schliessen{position:absolute;top:.5rem;right:.5rem;z-index:1;font:inherit;font-size:1.5rem;line-height:1;min-width:44px;min-height:44px;border-radius:var(--wks-i-radius,10px);border:1px solid #858f9e;background:#fff;color:#1f2937;cursor:pointer}', '@container wks (max-width:34rem){', '.wks-detail{grid-template-columns:minmax(0,1fr)}', '.wks-nav{flex-direction:column-reverse;align-items:stretch}.wks-nav .wks-btn{width:100%}', '.wks-zusammen{grid-template-columns:1fr;gap:0}.wks-zusammen dd{margin-bottom:.5rem}', '.wks-raster{grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.6rem}', '}', '@media (max-width:30rem){.wks-root{padding:16px}}', '@media (forced-colors:active){.wks-chip[aria-pressed="true"],.wks-daumen button[aria-pressed="true"]{outline:2px solid Highlight}}', '}' ].join('\n') function injectStyle () { if (document.getElementById(STYLE_ID)) return var st = document.createElement('style') st.id = STYLE_ID st.textContent = CSS document.head.appendChild(st) } function el (tag, cls, text) { var e = document.createElement(tag) if (cls) e.className = cls if (text != null) e.textContent = text return e } function knopf (cls, text, onClick) { var b = el('button', cls, text) b.type = 'button' if (onClick) b.addEventListener('click', onClick) return b } function httpsUrl (u) { if (typeof u !== 'string') return '' try { var p = new URL(u) return p.protocol === 'https:' ? p.href : '' } catch (e) { return '' } } function istText (v) { return typeof v === 'string' } function nichtLeer (v) { return typeof v === 'string' && v.replace(/\s+/g, '') !== '' } function istObjekt (v) { return v !== null && typeof v === 'object' && !Array.isArray(v) } function liste (v) { return Array.isArray(v) ? v : [] } function knoten (nl) { return Array.prototype.slice.call(nl) } // Pfade des Servers (ohne Host) bekommen die API-Adresse vorangestellt; nur Pfade, die mit genau einem / beginnen function absUrl (api, pfad) { if (!istText(pfad) || pfad.charAt(0) !== '/' || pfad.charAt(1) === '/') return '' return api + pfad } function apiBasis (root) { var a = String(root.getAttribute('data-api') || '').replace(/\/+$/, '') if (a) return a var s = SELF && SELF.src ? SELF : document.querySelector('script[src*="/embed/shop.js"]') if (s && s.src) { try { return new URL(s.src).origin } catch (e) { return '' } } return '' } function queryWert (name) { var m = new RegExp('[?&]' + name + '=([^&#]*)').exec(location.search || '') if (!m) return '' try { return decodeURIComponent(m[1].replace(/\+/g, ' ')) } catch (e) { return '' } } // Parameter aus der Adresse der Seite entfernen, ohne neu zu laden (nach der Rueckkehr von Stripe) function bereinigeUrl (namen) { try { var rest = String(location.search || '').replace(/^\?/, '').split('&').filter(function (p) { return p !== '' && namen.indexOf(p.split('=')[0]) < 0 }).join('&') history.replaceState(history.state, '', location.pathname + (rest ? '?' + rest : '') + (location.hash || '')) } catch (e) { /* Adresse bleibt, wie sie ist */ } } // @farben-anfang (reine Funktionen wie in embed/termine.js) // Relative Leuchtdichte nach WCAG 2.1 fuer '#rrggbb'. function luminanz (farbe) { var rgb = [1, 3, 5].map(function (i) { var c = parseInt(farbe.slice(i, i + 2), 16) / 255 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) }) return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2] } // Kontrastverhaeltnis zweier Leuchtdichten (1 bis 21). function kontrast (a, b) { return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05) } // Schrift auf der Akzentfarbe: Weiss oder das dunkle Grau, je nachdem was mehr Kontrast gibt; erreicht keines 4,5 : 1, nimmt sie Schwarz. function textFarbeAuf (farbe) { var l = luminanz(farbe) var weiss = kontrast(l, 1) var dunkel = kontrast(l, luminanz('#111827')) var beste = Math.max(weiss, dunkel) if (beste < 4.5 && kontrast(l, 0) > beste) return '#000000' return weiss >= dunkel ? '#ffffff' : '#111827' } // @farben-ende // Akzentfarbe des Shops (#rrggbb), wenn die Seite --wks-akzent nicht selbst setzt function setzeAkzent (box, farbe) { if (!/^#[0-9a-fA-F]{6}$/.test(farbe || '')) return if (getComputedStyle(box).getPropertyValue('--wks-akzent').trim()) return box.style.setProperty('--wks-vorgabe-akzent', farbe) box.style.setProperty('--wks-vorgabe-akzent-text', textFarbeAuf(farbe)) } // ---------------------------------------------------------------- Warenkorb im Browser (nur IDs und Mengen) function korbSchluessel (key) { return 'wks-korb-' + key } function korbLesen (key) { var aus = [] try { var roh = window.sessionStorage.getItem(korbSchluessel(key)) var arr = roh ? JSON.parse(roh) : [] if (Array.isArray(arr)) { arr.slice(0, MAX_POSITIONEN).forEach(function (z) { if (!Array.isArray(z) || z.length !== 3 || !istText(z[0]) || !ID_FORM.test(z[0]) || !istText(z[1]) || (z[1] !== '' && !ID_FORM.test(z[1]))) return var n = z[2] if (typeof n !== 'number' || n % 1 !== 0 || n < 1) return aus.push({ id: z[0], v: z[1], n: Math.min(n, MAX_MENGE) }) }) } } catch (e) { aus = [] } return aus } function korbSchreiben (key, korb) { try { if (korb.length === 0) window.sessionStorage.removeItem(korbSchluessel(key)) else window.sessionStorage.setItem(korbSchluessel(key), JSON.stringify(korb.map(function (z) { return [z.id, z.v, z.n] }))) } catch (e) { /* ohne Speicher bleibt der Warenkorb im Speicher der Seite */ } } // Zeilen eines Textes: Absaetze sind durch Leerzeilen getrennt function absaetze (text, cls) { var box = el('div', cls) String(text).split(/\n{2,}/).forEach(function (a) { if (a.trim()) box.appendChild(el('p', '', a.trim())) }) return box } function adresseText (a) { if (!istObjekt(a)) return '' var land = istText(a.land) ? (LAENDER[a.land] || a.land) : '' return [a.name, a.strasse, [a.plz, a.ort].filter(nichtLeer).join(' '), land].filter(nichtLeer).join(', ') } // ---------------------------------------------------------------- Ein Shop function init (root) { var key = root.getAttribute('data-webklar-shop') if (!key || root.getAttribute('data-wks-init')) return root.setAttribute('data-wks-init', '1') var api = apiBasis(root) var id = PREFIX + (++zaehler) + '-' var ausweich = null // Ausweichtext der Seite (Kindknoten des Containers), solange das Widget ihn ersetzt var dlg = null var dlgAusloeser = null var ui = { box: null, live: null, kopf: null, body: null, fuss: null, titel: null, statusBox: null } var S = null function neuerZustand () { return { cfg: S ? S.cfg : null, cfgAt: S ? S.cfgAt : 0, korb: S ? S.korb : [], ansicht: '', vorWiderruf: '', kategorie: '', produktId: '', auswahl: { v: '', n: 1 }, bild: 0, lieferart: '', land: '', pruef: null, pruefFehler: null, pruefSeq: 0, honig: '', fehler: {}, meldung: '', sendet: false, tot: false, fokusGesetzt: false, seq: 0, statusSeq: 0, status: null, wd: { name: '', email: '', bestellnummer: '', nachricht: '' }, wdSchritt: 'form', wdEingang: '' } } S = neuerZustand() function fid (name) { return id + String(name).replace(/[^a-z0-9_-]/gi, '-') } // ------------------------------------------------------------ Anfragen function request (method, pfad, body, frisch) { var opts = { method: method, credentials: 'omit', headers: { Accept: 'application/json' } } if (frisch) opts.cache = 'reload' // Konfiguration erneuern: nicht aus dem Browser-Cache (dort steht das alte Formular-Token) if (body !== undefined) { opts.headers['Content-Type'] = 'application/json' opts.body = JSON.stringify(body) } var ctrl = typeof AbortController === 'function' ? new AbortController() : null var timer = null if (ctrl) { opts.signal = ctrl.signal timer = setTimeout(function () { ctrl.abort() }, TIMEOUT_MS) } function fertig () { if (timer) clearTimeout(timer) } return fetch(api + API_PFAD + encodeURIComponent(key) + pfad, opts).then(function (r) { return r.json().then(function (d) { return { status: r.status, ok: r.ok, body: d } }, function () { return { status: r.status, ok: r.ok, body: null } }) }, function () { throw { netz: true } }).then(function (r) { fertig() if (r.ok && r.body !== null && typeof r.body === 'object') return r.body var fehler = {} var b = r.body if (b && typeof b.fehler === 'object' && b.fehler) { Object.keys(b.fehler).forEach(function (k) { if (istText(b.fehler[k])) fehler[k] = b.fehler[k] }) } throw { status: r.status, fehler: fehler, code: b && istText(b.code) ? b.code : '', produktId: b && istText(b.produktId) ? b.produktId : '', netz: r.status >= 500 || r.status === 403 || r.status === 404 || r.status === 0 } }, function (err) { fertig() throw err }) } // ------------------------------------------------------------ Konfiguration // Fail closed: fehlt etwas, das der Shop zwingend braucht, gilt er als nicht aktiv (der Ausweichtext der Seite bleibt) function konfigGueltig (d) { if (!d || d.aktiv !== true || !istObjekt(d.shop) || !nichtLeer(d.shop.name) || !nichtLeer(d.formToken)) return false if (d.nurWiderruf === true) return true if (typeof d.offen !== 'boolean' || !Array.isArray(d.produkte) || !istObjekt(d.shop.anbieter)) return false return d.produkte.every(function (p) { return istObjekt(p) && nichtLeer(p.id) && ID_FORM.test(p.id) && istText(p.name) && istText(p.preisText) }) } function lade (neu) { return request('GET', '', undefined, neu).then(function (d) { if (!konfigGueltig(d)) throw { inaktiv: true } S.cfg = d S.cfgAt = performance.now() return d }) } function configFrisch () { return !!S.cfg && performance.now() - S.cfgAt <= CONFIG_ALTER_MAX_MS } // Alte Konfiguration (das Formular-Token laeuft nach 2 h ab) erneuern. Aufgeloest wird mit true, wenn neu geladen wurde. // Ist der Server nicht erreichbar, geht es mit der alten Konfiguration weiter: der naechste Aufruf meldet es. function sichereConfig () { if (configFrisch()) return Promise.resolve(false) return lade(true).then(function () { bereinigeKorb(); return true }, function (err) { if (err && err.inaktiv) { zeigeAusweich(); throw err } return false }) } // Der Server speichert nichts (und antwortet doch mit Erfolg), wenn das Formular-Token juenger als 3 Sekunden ist: das Widget wartet vor dem // Absenden, bis es alt genug ist (gemessen ab Eintreffen der Konfiguration; das Token ist beim Server immer etwas aelter). function formWartezeitMs () { return Math.max(0, FORM_MIN_VORGABE_S * 1000 + FORM_PUFFER_MS - (performance.now() - S.cfgAt)) } // true = Token alt genug; false = die Ansicht hat sich inzwischen geaendert: nicht senden. function warteAufFormToken (senden) { var ms = formWartezeitMs() if (ms <= 0) return Promise.resolve(true) var zustand = S var seq = S.seq var beschriftung = senden.textContent senden.textContent = 'Einen Moment …' ansage('Einen Moment …') return new Promise(function (resolve) { setTimeout(function () { if (S !== zustand || S.seq !== seq || S.tot) { zustand.sendet = false; resolve(false); return } senden.textContent = beschriftung resolve(true) }, ms) }) } // ------------------------------------------------------------ Daten des Shops function shop () { return S.cfg.shop } function nurWiderruf () { return S.cfg.nurWiderruf === true } function rechtstexte () { return istObjekt(S.cfg.rechtstexte) ? S.cfg.rechtstexte : null } function gewaehr () { var g = S.cfg.gewaehrleistung return istObjekt(g) && nichtLeer(g.text) ? g : null } // Bestellen ist nur moeglich, wenn der Shop geoeffnet ist und die bestaetigten Rechtstexte samt Gewaehrleistungshinweis vorliegen function bestellbar () { var r = rechtstexte() return !nurWiderruf() && S.cfg.offen === true && !!r && nichtLeer(r.agb) && nichtLeer(r.widerrufsbelehrung) && !!gewaehr() } function produktMitId (pid) { var p = liste(S.cfg.produkte) for (var i = 0; i < p.length; i++) if (p[i].id === pid) return p[i] return null } function varianteMitId (p, vid) { var w = p && istObjekt(p.varianten) ? liste(p.varianten.werte) : [] for (var i = 0; i < w.length; i++) if (w[i].id === vid) return w[i] return null } function hatVarianten (p) { return !!p && istObjekt(p.varianten) && liste(p.varianten.werte).length > 0 } function anzahlImKorb () { return S.korb.reduce(function (s, z) { return s + z.n }, 0) } function speichereKorb () { korbSchreiben(key, S.korb) } // Positionen, die es nicht (mehr) gibt, fliegen aus dem Warenkorb; gibt die Anzahl entfernter Positionen zurueck function bereinigeKorb () { var vorher = S.korb.length S.korb = S.korb.filter(function (z) { var p = produktMitId(z.id) if (!p) return false if (hatVarianten(p)) return !!varianteMitId(p, z.v) return z.v === '' }) var weg = vorher - S.korb.length if (weg > 0) { speichereKorb() S.korbHinweis = true } return weg } function lieferInfoText () { var s = shop() var teile = [] var laender = liste(s.lieferlaender).map(function (l) { return istObjekt(l) ? l.name : '' }).filter(nichtLeer) if (laender.length) teile.push('Lieferung nach: ' + laender.join(', ')) if (istObjekt(s.abholung) && s.abholung.aktiv === true) teile.push('Abholung möglich') if (nichtLeer(s.zahlungsarten)) teile.push('Zahlung: ' + s.zahlungsarten) return teile.join(' · ') } function anbieterZeile () { var a = shop().anbieter if (!istObjekt(a)) return '' var teile = [adresseText(a)] if (nichtLeer(a.email)) teile.push('E-Mail: ' + a.email) if (nichtLeer(a.telefon)) teile.push('Telefon: ' + a.telefon) return teile.filter(nichtLeer).join(', ') } // ------------------------------------------------------------ Geruest function bauGeruest (box) { box.classList.add('wks-root') var live = el('div', 'wks-vh') live.setAttribute('aria-live', 'polite') live.setAttribute('aria-atomic', 'true') var kopf = el('div', 'wks-kopf') var body = el('div', 'wks-body') var fuss = el('div', 'wks-fuss') box.appendChild(live) box.appendChild(kopf) box.appendChild(body) box.appendChild(fuss) ui.box = box ui.live = live ui.kopf = kopf ui.body = body ui.fuss = fuss } function ansage (text) { if (ui.live) ui.live.textContent = text } function add (node) { ui.body.appendChild(node); return node } function titel (text) { var h = el('h3', 'wks-titel', text) h.tabIndex = -1 h.id = fid('titel') ui.titel = h return h } function meldungAnzeigen () { if (S.meldung) { add(el('p', 'wks-meldung', S.meldung)) ansage(S.meldung) } } function inBestellung (a) { return a === 'raster' || a === 'detail' || a === 'korb' || a === 'kasse' } function zeichneKopf () { ui.kopf.textContent = '' var zeile = el('div', 'wks-kopfzeile') zeile.appendChild(el('h2', 'wks-shopname', shop().name)) if (bestellbar() && S.ansicht !== 'status') { var n = anzahlImKorb() var kk = knopf('wks-btn wks-btn-sek', 'Warenkorb (' + n + ')', function () { gehe('korb') }) kk.setAttribute('aria-label', 'Warenkorb, ' + n + ' Stück') zeile.appendChild(kk) } ui.kopf.appendChild(zeile) if (bestellbar() && inBestellung(S.ansicht)) { var info = lieferInfoText() if (info) ui.kopf.appendChild(el('p', 'wks-lieferinfo', info)) } } function zeichne (fokus) { S.seq++ S.fokusGesetzt = false S.statusSeq++ ui.body.textContent = '' ui.titel = null zeichneKopf() var f = { raster: zeichneRaster, detail: zeichneDetail, korb: zeichneKorb, kasse: zeichneKasse, status: zeichneStatus, widerruf: zeichneWiderruf, geschlossen: zeichneGeschlossen } ;(f[S.ansicht] || zeichneRaster)() zeichneFuss() if (fokus) { if (ui.titel && !S.fokusGesetzt) ui.titel.focus() try { if (ui.box.getBoundingClientRect().top < 0) ui.box.scrollIntoView({ block: 'start' }) } catch (e) { /* Scrollen ist nur eine Hilfe */ } } } function gehe (ansicht, opt) { opt = opt || {} if (ansicht === 'widerruf' && S.ansicht !== 'widerruf') S.vorWiderruf = S.ansicht S.ansicht = ansicht S.fehler = opt.fehler || {} S.meldung = opt.meldung || '' zeichne(opt.fokus !== false) } function startAnsicht () { if (nurWiderruf()) return 'widerruf' return bestellbar() ? 'raster' : 'geschlossen' } // Fuss: immer AGB, Widerrufsbelehrung, Versand & Zahlung, Vertrag widerrufen, Impressum, Datenschutz (soweit bekannt) function zeichneFuss () { ui.fuss.textContent = '' var r = rechtstexte() var links = el('p', 'wks-fuss-links') if (r && nichtLeer(r.agb)) links.appendChild(knopf('wks-link', 'AGB', function (e) { zeigeAgb(e.currentTarget) })) if (r && nichtLeer(r.widerrufsbelehrung)) links.appendChild(knopf('wks-link', 'Widerrufsbelehrung', function (e) { zeigeBelehrung(e.currentTarget) })) if (r && nichtLeer(r.versandUndZahlung)) links.appendChild(knopf('wks-link', 'Versand & Zahlung', function (e) { zeigeVersand(e.currentTarget) })) if (!nurWiderruf()) links.appendChild(knopf('wks-btn wks-btn-sek', 'Vertrag widerrufen', zeigeWiderruf)) // im Widerrufs-Modus ist das Formular schon die ganze Ansicht var imp = r ? httpsUrl(r.impressumUrl) : '' if (imp) links.appendChild(externerLink('Impressum', imp)) var ds = r ? httpsUrl(r.datenschutzUrl) : '' if (ds) links.appendChild(externerLink('Datenschutz', ds)) ui.fuss.appendChild(links) var anb = anbieterZeile() if (anb) ui.fuss.appendChild(el('p', 'wks-anbieter', 'Anbieter: ' + anb)) ui.fuss.appendChild(el('p', 'wks-powered', 'Shop-Technik: WEBklar')) } function externerLink (text, href) { var a = el('a', '', text) a.href = href a.target = '_blank' a.rel = 'noopener noreferrer' return a } // Shop nicht erreichbar oder nicht aktiv: der Ausweichtext der Seite bleibt bzw. kommt zurueck function zeigeAusweich () { S.tot = true root.classList.remove('wks-root') root.textContent = '' if (ausweich) root.appendChild(ausweich.cloneNode(true)) // Kopie: ein zweiter Aufruf darf den Text nicht verlieren } // Netz weg oder Server kaputt mitten in der Strecke: Meldung und "Erneut versuchen"; nie ein selbststaendiger neuer Abruf function ausfall (wiederhole, meldung) { S.sendet = false ui.body.textContent = '' ui.titel = null var p = el('p', 'wks-fehlertext', meldung || 'Gerade nicht erreichbar. Bitte versuchen Sie es später noch einmal.') p.tabIndex = -1 add(p) add(knopf('wks-btn wks-btn-sek', 'Erneut versuchen', function () { wiederhole() })) ansage(p.textContent) p.focus() } function meldung429 (err) { return (err.fehler && err.fehler._) || 'Zu viele Anfragen. Bitte versuchen Sie es später noch einmal.' } // ------------------------------------------------------------ Dialog fuer Rechtstexte function dialogOeffnen (ueberschrift, bloecke, ausloeser) { if (!dlg) { dlg = el('dialog', 'wks-dialog') dlg.addEventListener('click', function (e) { if (e.target === dlg) dialogSchliessen() }) dlg.addEventListener('close', function () { if (dlgAusloeser && dlgAusloeser.focus) dlgAusloeser.focus() }) // Esc schliesst auch dort, wo showModal fehlt (der Browser erledigt es sonst selbst) dlg.addEventListener('keydown', function (e) { if (e.key === 'Escape' || e.key === 'Esc') { e.preventDefault(); dialogSchliessen() } }) root.appendChild(dlg) } dlgAusloeser = ausloeser || null dlg.className = 'wks-dialog' + (bloecke.some(function (b) { return !!b.bild }) ? ' wks-dialog-gross' : '') dlg.textContent = '' var innen = el('div', 'wks-dialog-innen') var zu = knopf('wks-schliessen', '×', dialogSchliessen) zu.setAttribute('aria-label', 'Schließen') innen.appendChild(zu) var h = el('h3', '', ueberschrift) h.tabIndex = -1 innen.appendChild(h) var text = el('div', 'wks-dialog-text') text.tabIndex = 0 // der Text scrollt: auch mit der Tastatur erreichbar text.setAttribute('role', 'region') text.setAttribute('aria-label', ueberschrift) bloecke.forEach(function (b) { if (b.bild) { var gross = el('img', 'wks-dialog-bild') gross.src = b.bild gross.alt = nichtLeer(b.text) ? b.text : '' gross.decoding = 'async' text.appendChild(gross) return } if (!nichtLeer(b.text)) return if (b.titel) text.appendChild(el('h4', 'wks-untertitel', b.titel)) text.appendChild(absaetze(b.text, '')) }) innen.appendChild(text) var nav = el('div', 'wks-nav') nav.appendChild(knopf('wks-btn wks-btn-sek', 'Schließen', dialogSchliessen)) innen.appendChild(nav) dlg.appendChild(innen) dlg.setAttribute('aria-label', ueberschrift) if (typeof dlg.showModal === 'function') { if (!dlg.open) dlg.showModal() } else { dlg.setAttribute('open', '') } h.focus() } function dialogSchliessen () { if (!dlg) return if (typeof dlg.close === 'function') dlg.close() else dlg.removeAttribute('open') if (dlgAusloeser && dlgAusloeser.focus) dlgAusloeser.focus() } function zeigeAgb (ausloeser) { var r = rechtstexte() if (r) dialogOeffnen('Allgemeine Geschäftsbedingungen', [{ text: r.agb }], ausloeser) } function zeigeBelehrung (ausloeser) { var r = rechtstexte() if (r) dialogOeffnen('Widerrufsbelehrung', [{ text: r.widerrufsbelehrung }, { titel: 'Muster-Widerrufsformular', text: r.widerrufsformular }], ausloeser) } function zeigeVersand (ausloeser) { var r = rechtstexte() var s = shop() var bloecke = [] if (nichtLeer(s.versandText)) bloecke.push({ titel: 'Versandkosten', text: s.versandText }) if (nichtLeer(s.mindestbestellText)) bloecke.push({ text: s.mindestbestellText }) if (nichtLeer(s.lieferzeit)) bloecke.push({ titel: 'Lieferzeit', text: s.lieferzeit }) if (r && nichtLeer(r.versandUndZahlung)) bloecke.push({ text: r.versandUndZahlung }) dialogOeffnen('Versand und Zahlung', bloecke, ausloeser) } // Versandhinweis neben dem Preis (PAngV § 6): nur wenn der Shop versendet; reine Abholshops zeigen kein "zzgl. Versand" function versandAktiv () { return liste(shop().lieferlaender).length > 0 } function versandLink () { return versandAktiv() ? knopf('wks-link', 'zzgl. Versand', function (e) { zeigeVersand(e.currentTarget) }) : null } // Hinweis auf das gesetzliche Gewaehrleistungsrecht (Art. 246a § 1 Abs. 1 S. 1 Nr. 11 EGBGB): amtliche Grafik in hervorgehobenem // Kasten vor der Zusammenfassung der Kasse, sofort geladen, mit dem woertlichen Text als Alternativtext und einer Vergroesserung function gewaehrKasten () { var g = gewaehr() if (!g) return null var kasten = el('section', 'wks-gewaehr') kasten.setAttribute('aria-label', 'Ihre gesetzlichen Gewährleistungsrechte') kasten.appendChild(el('h4', '', 'Ihre gesetzlichen Gewährleistungsrechte')) var bildUrl = httpsUrl(g.bild) if (bildUrl) { var img = el('img') img.src = bildUrl img.alt = g.text img.loading = 'eager' img.decoding = 'async' kasten.appendChild(img) kasten.appendChild(knopf('wks-btn wks-btn-sek', 'Vergrößern', function (e) { dialogOeffnen('Ihre gesetzlichen Gewährleistungsrechte', [{ bild: bildUrl, text: g.text }], e.currentTarget) })) } else { kasten.appendChild(absaetze(g.text, 'wks-text')) } return kasten } // ------------------------------------------------------------ Formularhelfer function honigfeld () { var wrap = el('div', 'wks-hp') wrap.setAttribute('aria-hidden', 'true') wrap.style.cssText = 'position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden' var lab = el('label', '', 'Bitte leer lassen') var inp = document.createElement('input') inp.type = 'text' inp.name = 'kontakt_url' inp.value = S.honig inp.setAttribute('autocomplete', 'off') inp.setAttribute('tabindex', '-1') inp.addEventListener('input', function () { S.honig = inp.value }) lab.appendChild(inp) wrap.appendChild(lab) return wrap } // opt: { name, label, typ (text|email|textarea), wert, pflicht, hinweis, autocomplete, maxlength, onInput }; refs sammelt { name: { input, fehler, beschreibung } } function feld (refs, opt) { var wrap = el('div', 'wks-feld') var fieldId = fid('f-' + opt.name) var lab = el('label', 'wks-label', opt.label) lab.htmlFor = fieldId if (opt.pflicht) { var st = el('span', 'wks-stern', ' *') st.setAttribute('aria-hidden', 'true') lab.appendChild(st) } else { lab.appendChild(document.createTextNode(' (freiwillig)')) } wrap.appendChild(lab) var beschreibung = [] if (opt.hinweis) { var h = el('p', 'wks-hinweis', opt.hinweis) h.id = fieldId + '-h' beschreibung.push(h.id) wrap.appendChild(h) } var inp if (opt.typ === 'textarea') { inp = document.createElement('textarea') inp.rows = 4 } else { inp = document.createElement('input') inp.type = opt.typ || 'text' } inp.id = fieldId inp.name = opt.name inp.className = 'wks-eingabe' inp.value = opt.wert || '' if (opt.pflicht) inp.required = true if (opt.autocomplete) inp.setAttribute('autocomplete', opt.autocomplete) if (opt.maxlength) inp.maxLength = opt.maxlength if (beschreibung.length) inp.setAttribute('aria-describedby', beschreibung.join(' ')) var fehler = el('p', 'wks-feldfehler') fehler.id = fieldId + '-e' fehler.hidden = true inp.addEventListener('input', function () { opt.onInput(inp.value) }) inp.addEventListener('change', function () { opt.onInput(inp.value) }) wrap.appendChild(inp) wrap.appendChild(fehler) refs[opt.name] = { input: inp, fehler: fehler, beschreibung: beschreibung } return wrap } // Fehlertexte an die Felder schreiben; liefert die Meldungen ohne Feld function zeigeFeldfehler (refs, fehler) { var uebrig = [] var erster = null Object.keys(refs).forEach(function (k) { var r = refs[k] var text = fehler[k] if (text) { r.fehler.textContent = text r.fehler.hidden = false r.input.setAttribute('aria-invalid', 'true') r.input.setAttribute('aria-describedby', r.beschreibung.concat([r.fehler.id]).join(' ')) if (!erster) erster = r.input } else { r.fehler.textContent = '' r.fehler.hidden = true r.input.removeAttribute('aria-invalid') if (r.beschreibung.length) r.input.setAttribute('aria-describedby', r.beschreibung.join(' ')) else r.input.removeAttribute('aria-describedby') } }) Object.keys(fehler).forEach(function (k) { if (!refs[k]) uebrig.push(fehler[k]) }) if (erster) { S.fokusGesetzt = true erster.focus() ansage('Bitte prüfen Sie Ihre Eingaben: ' + Object.keys(fehler).map(function (k) { return fehler[k] }).join(' ')) } return uebrig } // Meldungen ohne eigenes Feld in den Fehlerkasten: nie verschlucken function zeigeRest (box, texte) { texte.forEach(function (t) { box.appendChild(el('p', 'wks-fehlertext', t)) }) if (texte.length) ansage(texte.join(' ')) } function mengeFeld (wert, onChange, labelText, idName) { var wrap = el('div', 'wks-zeile-menge') var inp = document.createElement('input') inp.type = 'number' inp.min = '1' inp.max = String(MAX_MENGE) inp.step = '1' inp.className = 'wks-eingabe wks-eingabe-menge' inp.id = fid(idName) inp.value = String(wert) inp.setAttribute('inputmode', 'numeric') var lab = el('label', '', labelText) lab.htmlFor = inp.id inp.addEventListener('change', function () { var n = parseInt(inp.value, 10) if (!(n >= 1)) n = 1 if (n > MAX_MENGE) n = MAX_MENGE inp.value = String(n) onChange(n) }) wrap.appendChild(lab) wrap.appendChild(inp) return wrap } // ------------------------------------------------------------ Raster function preisAnzeige (p) { return nichtLeer(p.preisAbText) ? p.preisAbText : p.preisText } function bildElement (p, grosse, cls) { var b = liste(p.bilder)[0] var src = istObjekt(b) ? absUrl(api, b[grosse]) : '' if (!src) return null var img = el('img', cls) img.src = src img.alt = p.name img.loading = 'lazy' img.decoding = 'async' return img } function zeichneRaster () { var kategorien = liste(S.cfg.kategorien).filter(nichtLeer) add(titel('Unsere Artikel')) meldungAnzeigen() if (S.korbHinweis) { S.korbHinweis = false add(el('p', 'wks-meldung', 'Einige Artikel in Ihrem Warenkorb sind nicht mehr verfügbar und wurden entfernt.')) } var chips = null if (kategorien.length > 1) { chips = el('div', 'wks-chips') chips.setAttribute('role', 'group') chips.setAttribute('aria-label', 'Kategorien') add(chips) } var ul = el('ul', 'wks-raster') var leer = el('p', '', 'Zurzeit sind keine Artikel verfügbar.') function fuelle () { ul.textContent = '' var n = 0 liste(S.cfg.produkte).forEach(function (p) { if (S.kategorie && p.kategorie !== S.kategorie) return n++ ul.appendChild(karte(p)) }) ul.hidden = n === 0 leer.hidden = n !== 0 } if (chips) { var alle = [''] kategorien.forEach(function (k) { alle.push(k) }) alle.forEach(function (k) { var b = knopf('wks-chip', k || 'Alle', function () { S.kategorie = k knoten(chips.childNodes).forEach(function (c) { c.setAttribute('aria-pressed', String(c === b)) }) fuelle() ansage(k ? 'Kategorie ' + k : 'Alle Artikel') }) b.setAttribute('aria-pressed', String(S.kategorie === k)) chips.appendChild(b) }) } add(ul) add(leer) fuelle() // Steuerhinweis einmal unter dem Raster; der Versandhinweis steht bei jedem Preis (karte) if (nichtLeer(shop().steuerHinweisKurz)) add(el('p', 'wks-hinweiszeile', shop().steuerHinweisKurz)) } function karte (p) { var li = el('li') var art = el('div', 'wks-karte') var bild = bildElement(p, 'klein', 'wks-karte-bild') art.appendChild(bild || el('span', 'wks-karte-leer')) var h = el('h4', 'wks-karte-name') var b = knopf('wks-karte-knopf', p.name, function () { zeigeDetail(p.id) }) h.appendChild(b) art.appendChild(h) if (nichtLeer(p.kurz)) art.appendChild(el('p', 'wks-karte-text', p.kurz)) art.appendChild(el('p', 'wks-preis', preisAnzeige(p))) if (nichtLeer(p.grundpreisText)) art.appendChild(el('p', 'wks-grundpreis', p.grundpreisText)) var vl = versandLink() if (vl) { var vz = el('p', 'wks-preis-versand') vz.appendChild(vl) art.appendChild(vz) } if (p.ausverkauft === true || p.verfuegbar === false) art.appendChild(el('p', 'wks-aus', 'Ausverkauft')) li.appendChild(art) return li } // ------------------------------------------------------------ Detail function zeigeDetail (pid) { S.produktId = pid S.auswahl = { v: '', n: 1 } S.bild = 0 gehe('detail') } function herstellerZeilen (dl, p) { var si = istObjekt(p.sicherheit) ? p.sicherheit : {} var h = istObjekt(si.hersteller) ? si.hersteller : null function adr (a) { return [a.name, a.anschrift, a.kontakt].filter(nichtLeer).join(', ') } function zeile (l, w) { if (nichtLeer(w)) { dl.appendChild(el('dt', '', l)); dl.appendChild(el('dd', '', w)) } } if (h && nichtLeer(adr(h))) zeile('Hersteller', adr(h)) else zeile('Verkäufer und Verantwortlicher', anbieterZeile()) var eu = istObjekt(si.euVerantwortlicher) ? adr(si.euVerantwortlicher) : '' zeile('Verantwortliche Person in der EU', eu) zeile('Sicherheitshinweise', si.hinweise) } function zeichneDetail () { var p = produktMitId(S.produktId) if (!p) { gehe('raster'); return } var bilder = liste(p.bilder).filter(function (b) { return istObjekt(b) && absUrl(api, b.gross) }) var mitVar = hatVarianten(p) var verkaeuflich = p.ausverkauft !== true && p.verfuegbar !== false add(knopf('wks-btn wks-btn-sek', 'Zurück zur Übersicht', function () { gehe('raster') })) add(titel(p.name)) meldungAnzeigen() var gitter = el('div', 'wks-detail') var links = el('div', 'wks-galerie') if (bilder.length) { var gross = el('img', 'wks-galerie-gross') gross.alt = p.name gross.decoding = 'async' gross.src = absUrl(api, bilder[Math.min(S.bild, bilder.length - 1)].gross) links.appendChild(gross) if (bilder.length > 1) { var daumen = el('div', 'wks-daumen') bilder.forEach(function (b, i) { var db = el('button') db.type = 'button' db.setAttribute('aria-label', 'Bild ' + (i + 1) + ' von ' + bilder.length + ' anzeigen') db.setAttribute('aria-pressed', String(i === S.bild)) var di = el('img') di.src = absUrl(api, b.klein) || absUrl(api, b.gross) di.alt = '' di.loading = 'lazy' db.appendChild(di) db.addEventListener('click', function () { S.bild = i gross.src = absUrl(api, b.gross) knoten(daumen.childNodes).forEach(function (c, j) { c.setAttribute('aria-pressed', String(j === i)) }) }) daumen.appendChild(db) }) links.appendChild(daumen) } } gitter.appendChild(links) var rechts = el('div', 'wks-info') var preisEl = el('p', 'wks-preis') var gpEl = el('p', 'wks-grundpreis') var verfEl = el('p', verkaeuflich ? 'wks-klein' : 'wks-aus') rechts.appendChild(preisEl) rechts.appendChild(gpEl) var hinweis = el('p', 'wks-hinweiszeile') var dvl = versandLink() if (nichtLeer(shop().steuerHinweisKurz)) hinweis.appendChild(document.createTextNode(shop().steuerHinweisKurz + (dvl ? ' ' : ''))) if (dvl) hinweis.appendChild(dvl) if (hinweis.childNodes.length) rechts.appendChild(hinweis) verfEl.textContent = verkaeuflich ? 'Verfügbar' : 'Ausverkauft' rechts.appendChild(verfEl) var form = el('form', 'wks-form') form.noValidate = true var varSel = null var fehlerVar = el('p', 'wks-feldfehler') fehlerVar.hidden = true fehlerVar.id = fid('var-e') function aktualisierePreis () { var w = mitVar ? varianteMitId(p, S.auswahl.v) : null preisEl.textContent = w && nichtLeer(w.preisText) ? w.preisText : preisAnzeige(p) var gp = w && nichtLeer(w.grundpreisText) ? w.grundpreisText : (w ? '' : p.grundpreisText) gpEl.textContent = nichtLeer(gp) ? gp : '' gpEl.hidden = !nichtLeer(gp) if (w) { verfEl.textContent = w.verfuegbar === false ? 'Ausverkauft' : 'Verfügbar' verfEl.className = w.verfuegbar === false ? 'wks-aus' : 'wks-klein' } } if (mitVar) { var vf = el('div', 'wks-feld') var vl = el('label', 'wks-label', p.varianten.name) varSel = document.createElement('select') varSel.id = fid('variante') varSel.className = 'wks-eingabe' vl.htmlFor = varSel.id var o0 = document.createElement('option') o0.value = '' o0.textContent = 'Bitte wählen' varSel.appendChild(o0) liste(p.varianten.werte).forEach(function (w) { var o = document.createElement('option') o.value = w.id o.textContent = w.name + (nichtLeer(w.preisText) ? ' – ' + w.preisText : '') + (w.verfuegbar === false ? ' (ausverkauft)' : '') if (w.verfuegbar === false) o.disabled = true varSel.appendChild(o) }) varSel.value = S.auswahl.v varSel.addEventListener('change', function () { S.auswahl.v = varSel.value fehlerVar.hidden = true varSel.removeAttribute('aria-invalid') aktualisierePreis() }) vf.appendChild(vl) vf.appendChild(varSel) vf.appendChild(fehlerVar) form.appendChild(vf) } aktualisierePreis() var bestaetigung = el('p', 'wks-meldung') bestaetigung.hidden = true if (verkaeuflich) { var mf = el('div', 'wks-feld') var mw = mengeFeld(S.auswahl.n, function (n) { S.auswahl.n = n }, 'Menge', 'menge') mf.appendChild(mw) form.appendChild(mf) var plus = el('button', 'wks-btn', 'In den Warenkorb') plus.type = 'submit' form.appendChild(plus) form.addEventListener('submit', function (e) { e.preventDefault() if (mitVar && !S.auswahl.v) { fehlerVar.textContent = 'Bitte wählen Sie eine Variante (' + p.varianten.name + ').' fehlerVar.hidden = false varSel.setAttribute('aria-invalid', 'true') varSel.setAttribute('aria-describedby', fehlerVar.id) varSel.focus() ansage(fehlerVar.textContent) return } var n = parseInt(form.querySelector('input[type="number"]').value, 10) if (!(n >= 1)) n = 1 legeInKorb(p.id, mitVar ? S.auswahl.v : '', n) bestaetigung.textContent = p.name + ' wurde in den Warenkorb gelegt.' bestaetigung.hidden = false ansage(bestaetigung.textContent) zeichneKopf() }) } rechts.appendChild(form) rechts.appendChild(bestaetigung) var weiter = el('div', 'wks-nav') weiter.appendChild(knopf('wks-btn wks-btn-sek', 'Weiter einkaufen', function () { gehe('raster') })) weiter.appendChild(knopf('wks-btn wks-btn-sek', 'Zum Warenkorb', function () { gehe('korb') })) rechts.appendChild(weiter) gitter.appendChild(rechts) add(gitter) if (nichtLeer(p.beschreibung)) { add(el('h4', 'wks-untertitel', 'Beschreibung')) add(absaetze(p.beschreibung, 'wks-beschreibung')) } var fakten = el('dl', 'wks-zusammen') function fakt (l, w) { if (nichtLeer(w)) { fakten.appendChild(el('dt', '', l)); fakten.appendChild(el('dd', '', w)) } } fakt('Lieferzeit', shop().lieferzeit) fakt('Lieferart', p.lieferHinweis) fakt('Widerrufsrecht', p.widerrufText) if (fakten.childNodes.length) add(fakten) if (istObjekt(p.lebensmittel) && nichtLeer(p.lebensmittel.pflichtangaben)) { add(el('h4', 'wks-untertitel', 'Angaben nach Lebensmittelrecht')) add(absaetze(p.lebensmittel.pflichtangaben, 'wks-text')) } add(el('h4', 'wks-untertitel', 'Produktsicherheit')) var sicher = el('dl', 'wks-zusammen') herstellerZeilen(sicher, p) add(sicher) } function legeInKorb (pid, vid, n) { var gefunden = null S.korb.forEach(function (z) { if (z.id === pid && z.v === vid) gefunden = z }) if (gefunden) gefunden.n = Math.min(MAX_MENGE, gefunden.n + n) else if (S.korb.length < MAX_POSITIONEN) S.korb.push({ id: pid, v: vid, n: Math.min(MAX_MENGE, n) }) speichereKorb() S.pruef = null } // ------------------------------------------------------------ Warenkorb function zeichneKorb () { add(titel('Warenkorb')) meldungAnzeigen() if (S.korbHinweis) { S.korbHinweis = false add(el('p', 'wks-meldung', 'Einige Artikel in Ihrem Warenkorb sind nicht mehr verfügbar und wurden entfernt.')) } if (S.korb.length === 0) { add(el('p', '', 'Ihr Warenkorb ist leer.')) var nav0 = el('div', 'wks-nav') nav0.appendChild(knopf('wks-btn', 'Weiter einkaufen', function () { gehe('raster') })) add(nav0) return } var ul = el('ul', 'wks-zeilen') S.korb.forEach(function (z, i) { var p = produktMitId(z.id) var w = hatVarianten(p) ? varianteMitId(p, z.v) : null var li = el('li') var info = el('div', 'wks-zeile-info') info.appendChild(el('p', 'wks-zeile-name', p.name)) if (w) info.appendChild(el('p', 'wks-klein', p.varianten.name + ': ' + w.name)) var einzel = w && nichtLeer(w.preisText) ? w.preisText : p.preisText info.appendChild(el('p', 'wks-klein', 'Einzelpreis: ' + einzel)) var gp = w ? w.grundpreisText : p.grundpreisText if (nichtLeer(gp)) info.appendChild(el('p', 'wks-grundpreis', gp)) var ausv = w ? w.verfuegbar === false : (p.ausverkauft === true || p.verfuegbar === false) if (ausv) info.appendChild(el('p', 'wks-aus', 'Ausverkauft')) li.appendChild(info) li.appendChild(mengeFeld(z.n, function (n) { z.n = n; speichereKorb(); S.pruef = null; zeichneKopf() }, 'Menge', 'korb-menge-' + i)) var weg = knopf('wks-btn wks-btn-sek', 'Entfernen', function () { S.korb.splice(i, 1) speichereKorb() S.pruef = null S.meldung = p.name + ' wurde aus dem Warenkorb entfernt.' zeichne(false) if (ui.titel) { S.fokusGesetzt = true; ui.titel.focus() } }) weg.setAttribute('aria-label', p.name + ' aus dem Warenkorb entfernen') li.appendChild(weg) ul.appendChild(li) }) add(ul) add(el('p', 'wks-hinweiszeile', 'Die Preise, Versandkosten und die Gesamtsumme sehen Sie im nächsten Schritt.')) var nav = el('div', 'wks-nav') nav.appendChild(knopf('wks-btn wks-btn-sek', 'Weiter einkaufen', function () { gehe('raster') })) nav.appendChild(knopf('wks-btn', 'Zur Kasse', function () { gehe('kasse') })) add(nav) } // ------------------------------------------------------------ Kasse function kannVersenden () { var s = shop() if (liste(s.lieferlaender).length === 0) return false return !S.korb.some(function (z) { var p = produktMitId(z.id); return p && p.lieferart === 'abholung' }) } function kannAbholen () { return istObjekt(shop().abholung) && shop().abholung.aktiv === true } function positionenBody () { return S.korb.map(function (z) { var o = { produktId: z.id, menge: z.n } if (z.v) o.varianteId = z.v return o }) } function landCode () { var l = liste(shop().lieferlaender) if (S.land) return S.land return l.length && istObjekt(l[0]) ? l[0].code : '' } function zeichneKasse () { if (S.korb.length === 0) { gehe('korb'); return } var s = shop() var versand = kannVersenden() var abholung = kannAbholen() if (!versand && !abholung) { add(titel('Bestellung')) add(el('p', 'wks-fehlertext', 'Mit diesem Warenkorb ist keine Bestellung möglich. Bitte ändern Sie Ihren Warenkorb.')) add(knopf('wks-btn', 'Zum Warenkorb', function () { gehe('korb') })) return } if (S.lieferart !== 'versand' && S.lieferart !== 'abholung') S.lieferart = versand ? 'versand' : 'abholung' if (S.lieferart === 'versand' && !versand) S.lieferart = 'abholung' if (S.lieferart === 'abholung' && !abholung) S.lieferart = 'versand' if (!S.land) S.land = landCode() add(titel('Bestellung prüfen')) meldungAnzeigen() var form = el('form', 'wks-form') form.noValidate = true var fs = el('fieldset', 'wks-feld') fs.appendChild(el('legend', '', 'Lieferart')) var landWrap = el('div', 'wks-feld') var landSel = document.createElement('select') landSel.id = fid('land') landSel.className = 'wks-eingabe' var landLab = el('label', 'wks-label', 'Lieferland') landLab.htmlFor = landSel.id liste(s.lieferlaender).forEach(function (l) { if (!istObjekt(l)) return var o = document.createElement('option') o.value = l.code o.textContent = l.name landSel.appendChild(o) }) landSel.value = S.land landWrap.appendChild(landLab) landWrap.appendChild(landSel) function landSichtbar () { landWrap.hidden = S.lieferart !== 'versand' } function radio (wert, text) { var r = el('div', 'wks-radio') var inp = document.createElement('input') inp.type = 'radio' inp.name = fid('lieferart') inp.id = fid('la-' + wert) inp.value = wert inp.checked = S.lieferart === wert inp.addEventListener('change', function () { if (!inp.checked) return S.lieferart = wert landSichtbar() pruefe() }) var lab = el('label', '', text) lab.htmlFor = inp.id r.appendChild(inp) r.appendChild(lab) return r } if (versand) fs.appendChild(radio('versand', 'Versand')) if (abholung) { var abText = 'Abholung' if (nichtLeer(s.abholung.adresse)) abText += ' (' + s.abholung.adresse + ')' fs.appendChild(radio('abholung', abText)) if (nichtLeer(s.abholung.hinweis)) fs.appendChild(el('p', 'wks-hinweis', s.abholung.hinweis)) } form.appendChild(fs) form.appendChild(landWrap) landSichtbar() landSel.addEventListener('change', function () { S.land = landSel.value; pruefe() }) // Gewaehrleistungshinweis: hervorgehoben vor der Zusammenfassung (wird nicht mit der Zusammenfassung neu aufgebaut) var gkasten = gewaehrKasten() if (gkasten) form.appendChild(gkasten) // Pflichtinformationen: stehen unmittelbar ueber dem Knopf und kommen zum grossen Teil vom Server (/pruefen) var pflicht = el('div', 'wks-pflicht') form.appendChild(pflicht) form.appendChild(honigfeld()) var fehlerBox = el('div', 'wks-fehlerbox') form.appendChild(fehlerBox) // Einbeziehung der AGB (§ 305 Abs. 2 BGB): ausdruecklicher Hinweis unmittelbar ueber dem Knopf, beide Texte oeffnen sich im Dialog var agbHinweis = el('p', 'wks-agb-hinweis') agbHinweis.appendChild(document.createTextNode('Es gelten unsere ')) agbHinweis.appendChild(knopf('wks-link', 'AGB', function (e) { zeigeAgb(e.currentTarget) })) agbHinweis.appendChild(document.createTextNode('. Informationen zu Ihrem Widerrufsrecht finden Sie in der ')) agbHinweis.appendChild(knopf('wks-link', 'Widerrufsbelehrung', function (e) { zeigeBelehrung(e.currentTarget) })) agbHinweis.appendChild(document.createTextNode('.')) form.appendChild(agbHinweis) var senden = el('button', 'wks-btn', 'Zahlungspflichtig bestellen') senden.type = 'submit' senden.disabled = true var nav = el('div', 'wks-nav') nav.appendChild(knopf('wks-btn wks-btn-sek', 'Zurück zum Warenkorb', function () { gehe('korb') })) nav.appendChild(senden) form.appendChild(nav) add(form) function fuellePflicht () { pflicht.textContent = '' var ps = S.pruef if (S.pruefFehler) { pflicht.appendChild(el('p', 'wks-fehlertext', S.pruefFehler.text)) if (S.pruefFehler.zumWarenkorb) pflicht.appendChild(knopf('wks-btn wks-btn-sek', 'Warenkorb ändern', function () { gehe('korb') })) if (S.pruefFehler.erneut) pflicht.appendChild(knopf('wks-btn wks-btn-sek', 'Erneut versuchen', function () { pruefe() })) senden.disabled = true return } if (!ps) { pflicht.appendChild(el('p', 'wks-laden', 'Wird berechnet …')) senden.disabled = true return } pflicht.appendChild(el('h4', '', 'Ihre Bestellung')) var ul = el('ul', 'wks-pos') liste(ps.positionen).forEach(function (z) { var li = el('li') var t = el('span', 'wks-pos-text', z.menge + ' × ' + z.name + (nichtLeer(z.varianteText) ? ' (' + z.varianteText + ')' : '') + ', je ' + z.einzelText) li.appendChild(t) li.appendChild(el('span', '', z.summeText)) ul.appendChild(li) }) pflicht.appendChild(ul) var t = ps.texte var dl = el('dl', 'wks-summen') dl.appendChild(el('dt', '', 'Zwischensumme')) dl.appendChild(el('dd', '', t.zwischensumme)) dl.appendChild(el('dt', '', S.lieferart === 'abholung' ? 'Lieferart' : 'Versandkosten')) dl.appendChild(el('dd', '', t.versand)) dl.appendChild(el('dt', 'wks-gesamt', 'Gesamtsumme')) dl.appendChild(el('dd', 'wks-gesamt', t.gesamt)) pflicht.appendChild(dl) if (nichtLeer(t.steuerHinweis)) pflicht.appendChild(el('p', 'wks-klein', t.steuerHinweis)) else if (nichtLeer(s.steuerHinweisKurz)) pflicht.appendChild(el('p', 'wks-klein', s.steuerHinweisKurz)) if (nichtLeer(s.mindestbestellText)) pflicht.appendChild(el('p', 'wks-klein', s.mindestbestellText)) pflicht.appendChild(el('h4', '', 'Lieferung und Zahlung')) var lz = el('dl', 'wks-zusammen') function zeile (l, w) { if (nichtLeer(w)) { lz.appendChild(el('dt', '', l)); lz.appendChild(el('dd', '', w)) } } zeile('Lieferart', S.lieferart === 'abholung' ? 'Abholung' : 'Versand' + (S.land ? ' nach ' + landName(S.land) : '')) zeile('Lieferzeit', s.lieferzeit) zeile('Zahlungsarten', s.zahlungsarten) zeile('Anbieter', anbieterZeile()) pflicht.appendChild(lz) pflicht.appendChild(el('h4', '', 'Widerrufsrecht')) pflicht.appendChild(el('p', '', 'Sie haben ein gesetzliches Widerrufsrecht, soweit unten für einzelne Artikel nichts anderes steht. Einzelheiten finden Sie in der Widerrufsbelehrung.')) liste(ps.positionen).forEach(function (z) { var txt = nichtLeer(z.widerrufText) ? z.widerrufText : ((produktMitId(z.produktId) || {}).widerrufText) if (nichtLeer(txt)) pflicht.appendChild(el('p', '', z.name + ': ' + txt)) }) var links = el('p', 'wks-links') links.appendChild(knopf('wks-link', 'AGB lesen', function (e) { zeigeAgb(e.currentTarget) })) links.appendChild(knopf('wks-link', 'Widerrufsbelehrung lesen', function (e) { zeigeBelehrung(e.currentTarget) })) links.appendChild(knopf('wks-link', 'Versand & Zahlung lesen', function (e) { zeigeVersand(e.currentTarget) })) pflicht.appendChild(links) pflicht.appendChild(el('p', '', 'Name, Lieferadresse und Zahlung geben Sie im nächsten Schritt bei unserem Zahlungsdienstleister Stripe ein.')) senden.disabled = S.sendet } function landName (code) { var l = liste(shop().lieferlaender) for (var i = 0; i < l.length; i++) if (istObjekt(l[i]) && l[i].code === code) return l[i].name return code } // Zusammenfassung vom Server (nie im Browser gerechnet); bei jeder Aenderung neu, veraltete Antworten werden verworfen function pruefe () { var seq = ++S.pruefSeq var ansichtSeq = S.seq S.pruef = null S.pruefFehler = null fuellePflicht() request('POST', '/pruefen', { positionen: positionenBody(), lieferart: S.lieferart, land: landCode() }).then(function (d) { if (seq !== S.pruefSeq || ansichtSeq !== S.seq) return if (!d || d.ok !== true || !istObjekt(d.texte) || !Array.isArray(d.positionen)) throw { netz: true } S.pruef = d fuellePflicht() ansage('Die Zusammenfassung wurde aktualisiert. Gesamtsumme ' + d.texte.gesamt + '.') }).catch(function (err) { if (seq !== S.pruefSeq || ansichtSeq !== S.seq) return if (err && err.inaktiv) { zeigeAusweich(); return } if (err && err.status === 429) S.pruefFehler = { text: meldung429(err), erneut: true } else if (err && !err.netz && err.fehler && Object.keys(err.fehler).length) { S.pruefFehler = { text: Object.keys(err.fehler).map(function (k) { return err.fehler[k] }).join(' '), zumWarenkorb: true } } else S.pruefFehler = { text: 'Gerade nicht erreichbar. Bitte versuchen Sie es später noch einmal.', erneut: true } fuellePflicht() ansage(S.pruefFehler.text) }) } S.pruefSeq++ pruefe() form.addEventListener('submit', function (e) { e.preventDefault() if (S.sendet || !S.pruef || S.pruefFehler) return fehlerBox.textContent = '' bestelle(senden, fehlerBox) }) } // Verbindlich bestellen: der Server rechnet alles neu und legt die Bestellung an; die Antwort fuehrt zur Zahlung bei Stripe function bestelle (senden, fehlerBox) { S.sendet = true senden.disabled = true senden.setAttribute('aria-busy', 'true') sichereConfig().then(function (neu) { if (neu) { S.sendet = false gehe('kasse', { meldung: 'Der Shop wurde aktualisiert. Bitte prüfen Sie Ihre Bestellung noch einmal.' }) return null } return warteAufFormToken(senden).then(function (los) { if (!los) return null return request('POST', '/bestellung', { positionen: positionenBody(), lieferart: S.lieferart, land: landCode(), rueckUrl: location.href, formToken: S.cfg.formToken, kontakt_url: S.honig }).then(function (d) { var ziel = d ? httpsUrl(d.url) : '' if (!ziel) throw { netz: true } senden.textContent = 'Weiterleitung zu Stripe …' ansage('Sie werden zur Zahlung bei Stripe weitergeleitet.') location.href = ziel // S.sendet bleibt gesetzt: kein zweiter Klick waehrend der Weiterleitung }) }) }).catch(function (err) { S.sendet = false senden.removeAttribute('aria-busy') senden.disabled = false if (err && err.inaktiv) return nachSendeFehler(err, senden, fehlerBox) }) } function nachSendeFehler (err, senden, fehlerBox) { if (err && err.code === 'neu_laden') { // Formular-Token abgelaufen oder ungueltig: Konfiguration neu holen, dem Besucher Bescheid geben lade(true).then(function () { bereinigeKorb() gehe('kasse', { fokus: false, meldung: 'Das Formular wurde erneuert. Bitte versuchen Sie es noch einmal.' }) }, function (e2) { if (e2 && e2.inaktiv) zeigeAusweich(); else ausfall(function () { zeichne(true) }) }) return } if (err && err.status === 429) { err.fehler = { _: meldung429(err) } } if (err && !err.netz && err.fehler && Object.keys(err.fehler).length) { fehlerBox.textContent = '' zeigeRest(fehlerBox, Object.keys(err.fehler).map(function (k) { return err.fehler[k] })) return } ausfall(function () { zeichne(true) }) } // ------------------------------------------------------------ Rueckkehr von Stripe function zeigeRueckkehr (r) { if (r.art === 'abbruch') { S.ansicht = bestellbar() ? 'korb' : startAnsicht() S.meldung = 'Die Zahlung wurde abgebrochen. Ihre Bestellung wurde nicht abgeschlossen.' + (S.korb.length ? ' Ihr Warenkorb ist noch gefüllt.' : '') zeichne(true) return } S.ansicht = 'status' S.status = { id: r.id, t: r.t, daten: null, fehler: false, versuch: 0 } zeichne(true) holeStatus(S.statusSeq) } function holeStatus (seq) { var st = S.status st.versuch++ request('GET', '/bestellung/' + encodeURIComponent(st.id) + '?t=' + encodeURIComponent(st.t)).then(function (d) { if (seq !== S.statusSeq) return if (!d || !istText(d.status)) throw { netz: true } st.daten = d st.fehler = false if (d.status !== 'offen' && d.status !== 'storniert') { S.korb = [] speichereKorb() zeichneKopf() } fuelleStatus() if (d.status === 'offen' && st.versuch < STATUS_VERSUCHE) { setTimeout(function () { if (seq === S.statusSeq) holeStatus(seq) }, STATUS_ABSTAND_MS) } }).catch(function (err) { if (seq !== S.statusSeq) return if (err && err.inaktiv) return if (!st.daten) st.fehler = true fuelleStatus() }) } function zeichneStatus () { add(titel('Ihre Bestellung')) var box = el('div', 'wks-statusbox') box.id = fid('status') add(box) ui.statusBox = box add(knopf('wks-btn wks-btn-sek', 'Zurück zum Shop', function () { gehe(startAnsicht()) })) fuelleStatus() } function fuelleStatus () { var box = ui.statusBox var st = S.status if (!box || !st || S.ansicht !== 'status') return box.textContent = '' var d = st.daten var text = '' if (!d) { if (st.fehler) { box.appendChild(el('p', 'wks-fehlertext', 'Wir können den Stand Ihrer Bestellung gerade nicht abrufen. Wenn Sie bezahlt haben, erhalten Sie eine Bestätigung per E-Mail.')) text = box.textContent } else { box.appendChild(el('p', 'wks-laden', 'Ihre Bestellung wird geprüft …')) text = 'Ihre Bestellung wird geprüft.' } ansage(text) return } var nr = nichtLeer(d.bestellnummer) ? d.bestellnummer : '' if (d.status === 'storniert') { box.appendChild(el('p', 'wks-ergebnis-text', 'Ihre Bestellung' + (nr ? ' ' + nr : '') + ' wurde nicht abgeschlossen. Die Zahlung ist nicht erfolgt oder wurde abgebrochen.')) box.appendChild(el('p', '', 'Wenn Sie den Artikel noch möchten, legen Sie ihn bitte erneut in den Warenkorb.')) } else { box.appendChild(el('p', 'wks-ergebnis-text', 'Vielen Dank für Ihre Bestellung' + (nr ? ' ' + nr : '') + '!')) if (d.status === 'offen') { box.appendChild(el('p', '', 'Die Zahlung ist noch nicht bestätigt. Bei einer SEPA-Lastschrift kann das einige Tage dauern. Sobald sie bestätigt ist, erhalten Sie eine E-Mail.')) } else { box.appendChild(el('p', '', 'Ihre Zahlung ist eingegangen.' + (nichtLeer(d.email) ? ' Eine Bestätigung senden wir an ' + d.email + '.' : ' Eine Bestätigung senden wir Ihnen per E-Mail.'))) } var dl = el('dl', 'wks-zusammen') if (nr) { dl.appendChild(el('dt', '', 'Bestellnummer')); dl.appendChild(el('dd', '', nr)) } if (nichtLeer(d.gesamtText)) { dl.appendChild(el('dt', '', 'Gesamtsumme')); dl.appendChild(el('dd', '', d.gesamtText)) } if (d.lieferart === 'versand' || d.lieferart === 'abholung') { dl.appendChild(el('dt', '', 'Lieferart')); dl.appendChild(el('dd', '', d.lieferart === 'abholung' ? 'Abholung' : 'Versand')) } if (dl.childNodes.length) box.appendChild(dl) } ansage(box.textContent) } // ------------------------------------------------------------ Geschlossen function zeichneGeschlossen () { add(titel('Shop geschlossen')) add(el('p', 'wks-ergebnis-text', 'Der Shop ist gerade geschlossen.')) } // ------------------------------------------------------------ Widerruf (Widerrufsfunktion, § 356a BGB) function zeigeWiderruf () { S.wdSchritt = 'form' S.wdEingang = '' gehe('widerruf') } function eingangText (e) { if (istText(e)) return e if (istObjekt(e)) return [e.datum, nichtLeer(e.uhrzeit) ? e.uhrzeit + ' Uhr' : ''].filter(nichtLeer).join(', ') return '' } function zeichneWiderruf () { add(titel('Vertrag widerrufen')) var w = S.wd if (S.wdSchritt === 'fertig') { var e = eingangText(S.wdEingang) add(el('p', 'wks-ergebnis-text', 'Ihr Widerruf ist am ' + (e || 'heute') + ' bei uns eingegangen. Eine Eingangsbestätigung haben wir Ihnen per E-Mail geschickt.')) ansage('Ihr Widerruf ist bei uns eingegangen.') if (!nurWiderruf()) { var nav0 = el('div', 'wks-nav') nav0.appendChild(knopf('wks-btn wks-btn-sek', 'Zurück zum Shop', zurueckVomWiderruf)) add(nav0) } return } if (S.wdSchritt === 'pruefen') { add(el('p', '', 'Bitte prüfen Sie Ihre Angaben. Mit „Widerruf bestätigen“ erklären Sie den Widerruf Ihres Vertrags.')) var dl = el('dl', 'wks-zusammen') function zeile (l, v) { if (nichtLeer(v)) { dl.appendChild(el('dt', '', l)); dl.appendChild(el('dd', '', v)) } } zeile('Name', w.name.trim()) zeile('E-Mail-Adresse', w.email.trim()) zeile('Bestellnummer', w.bestellnummer.trim()) zeile('Erklärung', w.nachricht.trim()) add(dl) var fb = el('div', 'wks-fehlerbox') add(fb) var senden = el('button', 'wks-btn', 'Widerruf bestätigen') senden.type = 'button' var nav1 = el('div', 'wks-nav') nav1.appendChild(knopf('wks-btn wks-btn-sek', 'Angaben ändern', function () { S.wdSchritt = 'form'; zeichne(true) })) nav1.appendChild(senden) add(nav1) senden.addEventListener('click', function () { sendeWiderruf(senden, fb) }) return } add(el('p', '', 'Hier können Sie Ihren Vertrag widerrufen. Bitte geben Sie Ihren Namen, die E-Mail-Adresse Ihrer Bestellung und möglichst die Bestellnummer an. Sie steht in Ihrer Bestätigungs-E-Mail.')) meldungAnzeigen() var form = el('form', 'wks-form') form.noValidate = true var refs = {} form.appendChild(feld(refs, { name: 'name', label: 'Ihr Name', typ: 'text', wert: w.name, pflicht: true, autocomplete: 'name', maxlength: 100, onInput: function (v) { w.name = v } })) form.appendChild(feld(refs, { name: 'email', label: 'E-Mail-Adresse der Bestellung', typ: 'email', wert: w.email, pflicht: true, autocomplete: 'email', maxlength: 254, onInput: function (v) { w.email = v } })) form.appendChild(feld(refs, { name: 'bestellnummer', label: 'Bestellnummer (falls zur Hand)', typ: 'text', wert: w.bestellnummer, pflicht: false, maxlength: 40, hinweis: 'Ohne Bestellnummer: Nennen Sie bitte Bestelldatum und Artikel im Feld „Erklärung“, damit wir Ihren Vertrag zuordnen können.', onInput: function (v) { w.bestellnummer = v } })) form.appendChild(feld(refs, { name: 'nachricht', label: 'Erklärung', typ: 'textarea', wert: w.nachricht, pflicht: false, maxlength: 1000, hinweis: 'Zum Beispiel, welche Artikel Sie widerrufen. Ohne Angabe widerrufen Sie die gesamte Bestellung.', onInput: function (v) { w.nachricht = v } })) form.appendChild(honigfeld()) var fehlerBox = el('div', 'wks-fehlerbox') form.appendChild(fehlerBox) var weiter = el('button', 'wks-btn', 'Weiter') weiter.type = 'submit' var nav = el('div', 'wks-nav') if (!nurWiderruf()) nav.appendChild(knopf('wks-btn wks-btn-sek', 'Zurück zum Shop', zurueckVomWiderruf)) nav.appendChild(weiter) form.appendChild(nav) add(form) if (Object.keys(S.fehler).length) zeigeRest(fehlerBox, zeigeFeldfehler(refs, S.fehler)) form.addEventListener('submit', function (e) { e.preventDefault() var f = {} if (!w.name.trim()) f.name = 'Bitte geben Sie Ihren Namen an.' if (!EMAIL_FORM.test(w.email.trim())) f.email = 'Bitte geben Sie eine gültige E-Mail-Adresse an.' if (!w.bestellnummer.trim() && !w.nachricht.trim()) f.nachricht = 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.' fehlerBox.textContent = '' zeigeFeldfehler(refs, f) if (Object.keys(f).length) return S.fehler = {} S.wdSchritt = 'pruefen' zeichne(true) }) } function zurueckVomWiderruf () { S.wdSchritt = 'form' var z = S.vorWiderruf gehe(z && z !== 'widerruf' && z !== 'status' && z !== 'kasse' ? z : startAnsicht()) } function sendeWiderruf (senden, fehlerBox) { if (S.sendet) return var w = S.wd S.sendet = true senden.disabled = true senden.setAttribute('aria-busy', 'true') sichereConfig().then(function () { return warteAufFormToken(senden) }).then(function (los) { if (!los) return null return request('POST', '/widerruf', { name: w.name.trim(), email: w.email.trim(), bestellnummer: w.bestellnummer.trim(), nachricht: w.nachricht.trim(), formToken: S.cfg.formToken, kontakt_url: S.honig }).then(function (d) { S.sendet = false if (d && d.aktiv === false) { zeigeAusweich(); return } if (!d || d.ok !== true) throw { netz: true } S.wdEingang = d.eingang S.wdSchritt = 'fertig' S.wd = { name: '', email: '', bestellnummer: '', nachricht: '' } zeichne(true) }) }).catch(function (err) { S.sendet = false senden.removeAttribute('aria-busy') senden.disabled = false if (err && err.inaktiv) return if (err && err.code === 'neu_laden') { lade(true).then(function () { S.wdSchritt = 'form'; gehe('widerruf', { fokus: false, meldung: 'Das Formular wurde erneuert. Bitte senden Sie es noch einmal ab.' }) }, function () { ausfall(function () { zeichne(true) }) }) return } if (err && err.status === 429) err.fehler = { _: meldung429(err) } if (err && !err.netz && err.fehler && Object.keys(err.fehler).length) { var felder = ['name', 'email', 'bestellnummer', 'nachricht'] if (Object.keys(err.fehler).some(function (k) { return felder.indexOf(k) >= 0 })) { S.wdSchritt = 'form' gehe('widerruf', { fehler: err.fehler }) return } fehlerBox.textContent = '' zeigeRest(fehlerBox, Object.keys(err.fehler).map(function (k) { return err.fehler[k] })) return } ausfall(function () { zeichne(true) }) }) } // ------------------------------------------------------------ Start function starte () { var cfg = S.cfg var at = S.cfgAt S = neuerZustand() S.cfg = cfg S.cfgAt = at setzeAkzent(ui.box, shop().akzent) if (!nurWiderruf()) { S.korb = korbLesen(key) bereinigeKorb() } var r = rueckkehr if (r) { rueckkehr = null bereinigeUrl(['wk_bestellung', 'wk_t', 'wk_abbruch']) if (!nurWiderruf()) { zeigeRueckkehr(r); return } } var wunsch = root.getAttribute('data-ansicht') || queryAnsicht queryAnsicht = '' if (wunsch === 'widerruf') { gehe('widerruf', { fokus: false }); return } gehe(startAnsicht(), { fokus: false }) if (wunsch === 'agb' && rechtstexte() && nichtLeer(rechtstexte().agb)) zeigeAgb(null) } // Zurueck per Browser-Verlauf von Stripe (Seite aus dem Zwischenspeicher): der Knopf "Zahlungspflichtig bestellen" muss wieder gehen window.addEventListener('pageshow', function (e) { if (e.persisted && S && S.sendet && S.cfg && !S.tot) { S.sendet = false zeichne(false) } }) // Der Ausweichtext der Seite wird beiseite gelegt, sobald der Shop aktiv ist; scheitert der Aufbau, steht er wieder da lade().then(function () { injectStyle() ausweich = document.createDocumentFragment() while (root.firstChild) ausweich.appendChild(root.firstChild) try { bauGeruest(root) starte() } catch (e) { zeigeAusweich() } }, function () { /* nicht erreichbar oder nicht aktiv: der Ausweichtext der Seite bleibt unberuehrt stehen */ }) } function run () { var b = queryWert('wk_bestellung') var t = queryWert('wk_t') if (ID_FORM.test(b) && TOKEN_FORM.test(t)) rueckkehr = { art: 'status', id: b, t: t } else if (queryWert('wk_abbruch')) rueckkehr = { art: 'abbruch' } var a = queryWert('ansicht') if (a === 'widerruf' || a === 'agb') queryAnsicht = a var nodes = document.querySelectorAll('[data-webklar-shop]') for (var i = 0; i < nodes.length; i++) init(nodes[i]) } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run) else run() })()