/*! 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()
})()