Files
Webklar-Kundenbereich/public/embed/shop.js
T
ClaudeandClaude Opus 5.5 7104f97feb Shop Review B I-2, I-3: Gewaehrleistungskasten vor der Zusammenfassung, AGB-Hinweis ueber dem Bestellknopf
Die amtliche Gewaehrleistungsgrafik steht in hervorgehobenem Kasten mit Ueberschrift vor der
Zusammenfassung der Kasse (hoechstens 420 px breit, sofort geladen, Vergroessern oeffnet sie in
einem mit Esc und Knopf schliessbaren Dialog). Direkt ueber "Zahlungspflichtig bestellen" steht
"Es gelten unsere AGB. Informationen zu Ihrem Widerrufsrecht finden Sie in der
Widerrufsbelehrung." mit Links auf beide Texte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:47:28 +00:00

1781 lines
81 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*! WEBklar Webshop: Einbau-Skript fuer Kundenwebsites.
* <div data-webklar-shop="SITEKEY" data-api="https://project.webklar.com">Ausweichtext</div>
* 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-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)
}
// 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()
var hinweis = el('p', 'wks-hinweiszeile')
if (nichtLeer(shop().steuerHinweisKurz)) hinweis.appendChild(document.createTextNode(shop().steuerHinweisKurz + ' '))
hinweis.appendChild(knopf('wks-link', 'zzgl. Versand', function (e) { zeigeVersand(e.currentTarget) }))
add(hinweis)
}
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))
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')
if (nichtLeer(shop().steuerHinweisKurz)) hinweis.appendChild(document.createTextNode(shop().steuerHinweisKurz + ' '))
hinweis.appendChild(knopf('wks-link', 'zzgl. Versand', function (e) { zeigeVersand(e.currentTarget) }))
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()
})()