1(function () { 2 'use strict'; 3 4 const STORAGE_KEY = 'consent_state_v1'; 5 // Bump whenever a service is added to or removed from a category, or a category's purpose text 6 // changes: a decision stored under another version is void and the banner is shown again. 7 const CONSENT_VERSION = '2026-09-16'; 8 const CATEGORIES = ['necessary', 'functional', 'analytics', 'marketing']; 9 const OPTIONAL_CATEGORIES = ['functional', 'analytics', 'marketing']; 10 // A stored decision is re-requested after 12 months (retention period stated in the privacy notice). 11 const CONSENT_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1000; 12 // First-party cookies set on this site's domain by the services of each category. Cookies that 13 // providers set on their own domains (e.g. _GRECAPTCHA on google.com) cannot be removed from here. 14 const CATEGORY_COOKIE_PREFIXES = { 15 functional: [], 16 analytics: ['_ga', '_gid', '_gat', '_clck', '_clsk'], 17 marketing: ['_lfa'] 18 }; 19 20 function getLang() { 21 const lang = (document.documentElement.getAttribute('lang') || 'en').toLowerCase(); 22 return lang === 'de' ? 'de' : 'en'; 23 } 24 25 function getUiText(lang) { 26 if (lang === 'de') { 27 return { 28 title: 'Cookie-Einstellungen', 29 description: 'Bitte wählen Sie, welche optionalen Dienste erlaubt sind. Sie werden erst nach Ihrer Einwilligung geladen; die Einwilligung können Sie jederzeit über âCookie-Einstellungenâ im Footer widerrufen. Google Search Console und Microsoft/Bing Webmaster Tools setzen keine Cookies und sind daher hier nicht aufgeführt.', 30 necessary: 'Technisch notwendig (immer aktiv): Sitzungs-Cookie zum Schutz der Formulare und Speicherung dieser Auswahl', 31 functional: 'Funktional: zusätzlicher Spam-Schutz der Kontaktformulare (Google reCAPTCHA); Datenübermittlung in die USA möglich', 32 analytics: 'Analyse: Reichweitenmessung und Sitzungsaufzeichnung (Google Analytics 4, Microsoft Clarity); Datenübermittlung in die USA möglich', 33 marketing: 'Marketing: Erkennung besuchender Unternehmen (Leadfeeder; technische Verarbeitung durch Quant Weave Capital GmbH); Datenübermittlung in die USA möglich', 34 privacyLink: 'Datenschutzerklärung', 35 privacyHref: 'Datenschutz.php', 36 imprintLink: 'Impressum', 37 imprintHref: 'Impressum.php', 38 rejectAll: 'Alle ablehnen', 39 acceptAll: 'Alle akzeptieren', 40 save: 'Auswahl speichern' 41 }; 42 } 43 44 return { 45 title: 'Cookie settings', 46 description: 'Choose which optional services are allowed. They load only after you consent; you can withdraw consent at any time via "Cookie settings" in the footer. Google Search Console and Microsoft/Bing Webmaster Tools set no cookies and are therefore not listed here.', 47 necessary: 'Strictly necessary (always on): session cookie protecting the forms, and storage of this choice', 48 functional: 'Functional: additional spam protection for the contact forms (Google reCAPTCHA); data may be transferred to the USA', 49 analytics: 'Analytics: audience measurement and session recording (Google Analytics 4, Microsoft Clarity); data may be transferred to the USA', 50 marketing: 'Marketing: identification of visiting companies (Leadfeeder; technical processing by Quant Weave Capital GmbH); data may be transferred to the USA', 51 privacyLink: 'Privacy Notice', 52 privacyHref: 'PrivacyNotice.php', 53 imprintLink: 'Imprint', 54 imprintHref: 'Imprint.php', 55 rejectAll: 'Reject all', 56 acceptAll: 'Accept all', 57 save: 'Save preferences' 58 }; 59 } 60 61 function nowIso() { 62 return new Date().toISOString(); 63 } 64 65 function safeParseJson(raw) { 66 if (!raw) return null; 67 try { 68 return JSON.parse(raw); 69 } catch { 70 return null; 71 } 72 } 73 74 function readConsent() { 75 const parsed = safeParseJson(localStorage.getItem(STORAGE_KEY)); 76 if (!parsed || !parsed.categories) return null; 77 // A decision made for a different set of categories, or older than the retention period, is void. 78 if (parsed.version !== CONSENT_VERSION) return null; 79 const decidedAt = Date.parse(parsed.ts); 80 if (Number.isNaN(decidedAt) || Date.now() - decidedAt > CONSENT_MAX_AGE_MS) return null; 81 return parsed; 82 } 83 84 function normalizeCategories(input) { 85 return { 86 necessary: true, 87 functional: !!input.functional, 88 analytics: !!input.analytics, 89 marketing: !!input.marketing 90 }; 91 } 92 93 function writeConsent(categories, source) { 94 const record = { 95 ts: nowIso(), 96 version: CONSENT_VERSION, 97 source: source || 'banner', 98 gpc: navigator.globalPrivacyControl === true, 99 categories: normalizeCategories(categories) 100 };
101 localStorage.setItem(STORAGE_KEY, JSON.stringify(record)); 102 return record; 103 } 104 105 function hasConsent(category) { 106 if (CATEGORIES.indexOf(category) === -1) return false; 107 const consent = readConsent(); 108 return !!(consent && consent.categories && consent.categories[category] === true); 109 } 110 111 function dispatchConsentEvent() { 112 const event = new CustomEvent('consent:updated', { detail: readConsent() }); 113 window.dispatchEvent(event); 114 } 115 116 function loadDeferredScripts() { 117 const nodes = document.querySelectorAll('script[type="text/plain"][data-consent-category]:not([data-consent-loaded="true"])'); 118 nodes.forEach(function (node) { 119 const category = node.getAttribute('data-consent-category'); 120 if (!hasConsent(category)) return; 121 122 const script = document.createElement('script'); 123 script.defer = true; 124 125 if (node.dataset.src) script.src = node.dataset.src; 126 if (node.textContent && node.textContent.trim()) script.text = node.textContent; 127 128 if (node.dataset.referrerpolicy) script.referrerPolicy = node.dataset.referrerpolicy; 129 if (node.dataset.crossorigin) script.crossOrigin = node.dataset.crossorigin; 130 131 document.head.appendChild(script); 132 node.setAttribute('data-consent-loaded', 'true'); 133 }); 134 } 135 136 function removeBanner() { 137 const banner = document.getElementById('cookie-consent-banner'); 138 if (banner) banner.remove(); 139 } 140 141 function withdrawnCategories(previous, next) { 142 if (!previous) return []; 143 return OPTIONAL_CATEGORIES.filter(function (category) { 144 return previous.categories[category] === true && !next[category]; 145 }); 146 } 147 148 function expireCookie(name) { 149 // Cookies may have been set for the exact host or for any parent domain, so try each. 150 const expired = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; 151 document.cookie = expired; 152 const parts = location.hostname.split('.'); 153 for (let i = 0; i < parts.length - 1; i++) { 154 document.cookie = expired + '; domain=.' + parts.slice(i).join('.'); 155 } 156 } 157 158 function clearCategoryCookies(categories) { 159 const prefixes = categories.reduce(function (list, category) { 160 return list.concat(CATEGORY_COOKIE_PREFIXES[category] || []); 161 }, []); 162 if (prefixes.length === 0) return; 163 164 document.cookie.split(';').forEach(function (entry) { 165 const name = entry.split('=')[0].trim(); 166 if (prefixes.some(function (prefix) { return name.indexOf(prefix) === 0; })) { 167 expireCookie(name); 168 } 169 }); 170 } 171 172 function applyDecision(categories, source) { 173 const withdrawn = withdrawnCategories(readConsent(), categories); 174 writeConsent(categories, source); 175 removeBanner(); 176 177 if (withdrawn.length > 0) { 178 // Services already running on this page cannot be switched off; a reload starts the page 179 // without them, since loadDeferredScripts() then skips the withdrawn categories. 180 clearCategoryCookies(withdrawn); 181 location.reload(); 182 return; 183 } 184 185 loadDeferredScripts(); 186 dispatchConsentEvent(); 187 } 188 189 function buildToggle(name, label, checked, disabled) { 190 const wrapper = document.createElement('label'); 191 wrapper.className = 'consent-banner__toggle'; 192 wrapper.htmlFor = 'consent-' + name; 193 194 const input = document.createElement('input'); 195 input.type = 'checkbox'; 196 input.id = 'consent-' + name; 197 input.name = name; 198 input.checked = !!checked; 199 input.disabled = !!disabled; 200 201 const text = document.createElement('span'); 202 text.textContent = label; 203 204 wrapper.appendChild(input); 205 wrapper.appendChild(text); 206 return wrapper; 207 } 208 209 function buildLink(href, label) { 210 const link = document.createElement('a'); 211 link.href = href; 212 link.textContent = label; 213 return link; 214 } 215 216 function showBanner() { 217 if (document.getElementById('cookie-consent-banner')) return; 218 219 const lang = getLang(); 220 const t = getUiText(lang); 221 222 const banner = document.createElement('section'); 223 banner.id = 'cookie-consent-banner'; 224 banner.className = 'consent-banner'; 225 banner.setAttribute('aria-label', t.title); 226 banner.setAttribute('aria-live', 'polite'); 227 228 const title = document.createElement('h2'); 229 title.className = 'consent-banner__title'; 230 title.textContent = t.title; 231 232 const description = document.createElement('p'); 233 description.className = 'consent-banner__description'; 234 description.textContent = t.description; 235 236 const form = document.createElement('form'); 237 form.className = 'consent-banner__form'; 238 form.id = 'cookie-consent-form'; 239 240 const saved = readConsent(); 241 const current = saved ? saved.categories : {}; 242 243 // Reject all and accept all sit above the category list, so both stay on the first screen 244 // of the banner even on small phones; the per-category save follows the toggles. 245 const quickActions = document.createElement('div'); 246 quickActions.className = 'consent-banner__actions'; 247 const actions = document.createElement('div'); 248 actions.className = 'consent-banner__actions'; 249 250 const rejectAll = document.createElement('button'); 251 rejectAll.type = 'button'; 252 rejectAll.id = 'consent-reject-all'; 253 rejectAll.textContent = t.rejectAll; 254 255 const acceptAll = document.createElement('button'); 256 acceptAll.type = 'button'; 257 acceptAll.id = 'consent-accept-all'; 258 acceptAll.textContent = t.acceptAll; 259 260 const save = document.createElement('button'); 261 save.type = 'submit'; 262 save.id = 'consent-save'; 263 save.textContent = t.save; 264 265 quickActions.appendChild(rejectAll); 266 quickActions.appendChild(acceptAll); 267 actions.appendChild(save); 268 269 form.appendChild(quickActions); 270 form.appendChild(buildToggle('necessary', t.necessary, true, true)); 271 form.appendChild(buildToggle('functional', t.functional, !!current.functional, false));
272 form.appendChild(buildToggle('analytics', t.analytics, !!current.analytics, false)); 273 form.appendChild(buildToggle('marketing', t.marketing, !!current.marketing, false)); 274 form.appendChild(actions); 275 276 const legal = document.createElement('p'); 277 legal.className = 'consent-banner__legal'; 278 legal.appendChild(buildLink(t.privacyHref, t.privacyLink)); 279 legal.appendChild(document.createTextNode(' | ')); 280 legal.appendChild(buildLink(t.imprintHref, t.imprintLink)); 281 282 banner.appendChild(title); 283 banner.appendChild(description); 284 banner.appendChild(form); 285 banner.appendChild(legal); 286 // First in the DOM so keyboard and screen-reader users reach the decision before the page 287 // content; the banner is still positioned at the bottom of the viewport by CSS. 288 document.body.prepend(banner); 289 290 rejectAll.addEventListener('click', function () { 291 applyDecision({ functional: false, analytics: false, marketing: false }, 'reject_all'); 292 }); 293 294 acceptAll.addEventListener('click', function () { 295 applyDecision({ functional: true, analytics: true, marketing: true }, 'accept_all'); 296 }); 297 298 form.addEventListener('submit', function (event) { 299 event.preventDefault(); 300 applyDecision({ 301 functional: !!form.querySelector('[name="functional"]').checked, 302 analytics: !!form.querySelector('[name="analytics"]').checked, 303 marketing: !!form.querySelector('[name="marketing"]').checked 304 }, 'custom'); 305 }); 306 } 307 308 function openSettings(event) { 309 if (event) event.preventDefault(); 310 showBanner(); 311 } 312 313 function bindFooterLinks() { 314 document.querySelectorAll('[data-open-cookie-settings="true"]').forEach(function (link) { 315 link.addEventListener('click', openSettings); 316 }); 317 } 318 319 function init() { 320 const consent = readConsent(); 321 const gpcEnabled = navigator.globalPrivacyControl === true; 322 323 if (!consent) { 324 if (gpcEnabled) { 325 applyDecision({ functional: false, analytics: false, marketing: false }, 'gpc'); 326 } else { 327 showBanner(); 328 } 329 } else { 330 loadDeferredScripts(); 331 } 332 333 bindFooterLinks(); 334 } 335 336 document.addEventListener('DOMContentLoaded', init); 337})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.