1/* 2 * holzfilm Consent Manager 3 * Consent-before-load for GTM and Vimeo, service-level choices and revocation. 4 */ 5(function () { 6 "use strict"; 7 8 const STORAGE_KEY = "holzfilm_consent_v3"; 9 const CONSENT_VERSION = 3; 10 const MAX_AGE_MS = 180 * 24 * 60 * 60 * 1000; 11 const GTM_ID = "GTM-WQW3SHD"; 12 const services = ["analytics", "hotjar", "googleAds", "facebook", "linkedIn", "rb2b", "vimeo"]; 13 const trackingServices = services.filter((name) => name !== "vimeo"); 14 const currentScriptUrl = document.currentScript?.src ? new URL(document.currentScript.src, location.href) : new URL(location.href); 15 const baseUrl = new URL("./", currentScriptUrl); 16 const legalUrl = (page) => new URL(`${page}/index.html`, baseUrl).href; 17 18 window.dataLayer = window.dataLayer || []; 19 window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; 20 window.gtag("consent", "default", { 21 ad_storage: "denied", 22 analytics_storage: "denied", 23 ad_user_data: "denied", 24 ad_personalization: "denied", 25 functionality_storage: "granted", 26 security_storage: "granted", 27 wait_for_update: 500 28 }); 29 30 const blankChoices = () => Object.fromEntries(services.map((name) => [name, false])); 31 32 function normalizeChoices(value) { 33 const normalized = blankChoices(); 34 services.forEach((name) => { normalized[name] = Boolean(value?.[name]); }); 35 return normalized; 36 } 37 38 function readDecision() { 39 try { 40 const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null"); 41 if (!parsed || parsed.version !== CONSENT_VERSION || !parsed.decidedAt) return null; 42 if (Date.now() - new Date(parsed.decidedAt).getTime() > MAX_AGE_MS) return null; 43 return { ...parsed, choices: normalizeChoices(parsed.choices) }; 44 } catch (_) { 45 return null; 46 } 47 } 48 49 function writeDecision(choices, source) { 50 const decision = { 51 version: CONSENT_VERSION, 52 decidedAt: new Date().toISOString(), 53 source, 54 choices: normalizeChoices(choices) 55 }; 56 try { localStorage.setItem(STORAGE_KEY, JSON.stringify(decision)); } catch (_) {} 57 return decision; 58 } 59 60 let decision = readDecision(); 61 let gtmLoaded = false; 62 let layer = null; 63 let lastFocus = null; 64 let requestedService = null; 65 66 function has(service) { 67 return Boolean(decision?.choices?.[service] && Date.now() - new Date(decision.decidedAt).getTime() < MAX_AGE_MS); 68 } 69 function anyTracking(choices) { return trackingServices.some((name) => Boolean(choices[name])); } 70 71 function serviceForResource(value) { 72 if (!value) return null; 73 let url; 74 try { url = new URL(String(value), location.href); } catch (_) { return null; } 75 const host = url.hostname.toLowerCase(); 76 const path = url.pathname.toLowerCase(); 77 78 if (host === "www.googletagmanager.com" && path.endsWith("/gtm.js")) return null; 79 if (host === "www.googletagmanager.com" && path.endsWith("/gtag/js")) { 80 const id = (url.searchParams.get("id") || "").toUpperCase(); 81 if (id.startsWith("AW-")) return "googleAds"; 82 if (id.startsWith("G-") || id.startsWith("UA-")) return "analytics"; 83 } 84 if (/googleadservices\.com$|doubleclick\.net$/.test(host)) return "googleAds"; 85 if (/hotjar\.com$|hotjar\.io$/.test(host)) return "hotjar"; 86 if (/facebook\.net$|facebook\.com$/.test(host)) return "facebook"; 87 if (/linkedin\.com$|licdn\.com$/.test(host)) return "linkedIn"; 88 if (/rb2b\.com$|retention\.com$/.test(host) || host === "ddwl4m2hdecbv.cloudfront.net") return "rb2b"; 89 return null; 90 } 91 92 function shouldBlockNode(node) { 93 if (!node || node.nodeType !== 1 || !/^(SCRIPT|IMG|IFRAME)$/.test(node.tagName)) return false; 94 const service = serviceForResource(node.src || node.getAttribute?.("src")); 95 return Boolean(service && !has(service)); 96 } 97 98 (function installResourceGuard() { 99 const nativeAppendChild = Node.prototype.appendChild; 100 const nativeInsertBefore = Node.prototype.insertBefore; 101 const nativeAppend = Element.prototype.append; 102 const nativePrepend = Element.prototype.prepend; 103 104 Node.prototype.appendChild = function (node) { 105 if (shouldBlockNode(node)) return node; 106 return nativeAppendChild.call(this, node); 107 }; 108 Node.prototype.insertBefore = function (node, reference) { 109 if (shouldBlockNode(node)) return node; 110 return nativeInsertBefore.call(this, node, reference); 111 }; 112 Element.prototype.append = function (...nodes) { 113 return nativeAppend.apply(this, nodes.filter((node) => !shouldBlockNode(node))); 114 }; 115 Element.prototype.prepend = function (...nodes) { 116 return nativePrepend.apply(this, nodes.filter((node) => !shouldBlockNode(node))); 117 }; 118 })(); 119 120 function pushLegacyEvents(choices) { 121 const events = { 122 analytics: ["AnalyticsOptIn", "AnalyticsOptOut"], 123 hotjar: ["HotjarOptIn", "HotjarOptOut"], 124 googleAds: ["GoogleAdsOptIn", "GoogleAdsOptOut"], 125 facebook: ["PixelOptIn", "PixelOptOut"], 126 linkedIn: ["LinkedInOptIn", "LinkedInOptOut"], 127 rb2b: ["RB2BOptIn", "RB2BOptOut"], 128 vimeo: ["VimeoOptIn", "VimeoOptOut"] 129 };
130 Object.entries(events).forEach(([name, names]) => window.dataLayer.push({ event: choices[name] ? names[0] : names[1] })); 131 } 132 133 function updateGoogleConsent(choices) { 134 window.gtag("consent", "update", { 135 analytics_storage: choices.analytics ? "granted" : "denied", 136 ad_storage: choices.googleAds ? "granted" : "denied", 137 ad_user_data: choices.googleAds ? "granted" : "denied", 138 // This campaign measures leads; it does not build remarketing audiences. 139 ad_personalization: "denied" 140 }); 141 } 142 143 function loadGtm() { 144 if (gtmLoaded || document.querySelector(`script[data-hf-gtm="${GTM_ID}"]`)) return; 145 gtmLoaded = true; 146 window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" }); 147 const script = document.createElement("script"); 148 script.async = true; 149 script.dataset.hfGtm = GTM_ID; 150 script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(GTM_ID)}`; 151 document.head.appendChild(script); 152 } 153 154 function applyDecision(nextDecision) { 155 decision = nextDecision; 156 updateGoogleConsent(decision.choices); 157 pushLegacyEvents(decision.choices); 158 if (anyTracking(decision.choices)) loadGtm(); 159 document.dispatchEvent(new CustomEvent("holzfilm:consentchange", { detail: { ...decision } })); 160 } 161 162 function expireCookie(name, domain, path = "/") { 163 const domainPart = domain ? `; domain=${domain}` : ""; 164 document.cookie = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}${domainPart}; SameSite=Lax`; 165 } 166 167 function clearKnownCookies(next) { 168 const denied = (name) => !next?.[name]; 169 if (denied("googleAds")) { 170 try { 171 localStorage.removeItem("holzfilm_ads_attribution_v1"); 172 localStorage.removeItem("_gcl_ls"); 173 } catch (_) {} 174 } 175 // A withdrawal must not erase another, still permitted service's cookies. 176 const cookiesByService = { 177 analytics: [/^_ga/, /^_gid$/, /^_gat/], 178 googleAds: [/^_gcl/], 179 facebook: [/^_fbp$/, /^_fbc$/, /^fb-pixel-is-disabled$/], 180 hotjar: [/^_hj/], 181 linkedIn: [/^li_/, /^bcookie$/, /^bscookie$/, /^lidc$/, /^UserMatchHistory$/, /^AnalyticsSyncHistory$/], 182 rb2b: [/^_reb2/i, /^_rb2b/i] 183 }; 184 const known = Object.entries(cookiesByService) 185 .filter(([service]) => denied(service)) 186 .flatMap(([, patterns]) => patterns); 187 const host = location.hostname; 188 const root = host.split(".").slice(-2).join("."); 189 document.cookie.split(";").forEach((part) => { 190 const name = part.split("=")[0].trim(); 191 if (!known.some((pattern) => pattern.test(name))) return; 192 const paths = new Set(["/"]); 193 const segments = location.pathname.split("/").filter(Boolean); 194 segments.forEach((_, index) => { 195 const path = `/${segments.slice(0, index + 1).join("/")}`; 196 paths.add(path); 197 paths.add(`${path}/`); 198 }); 199 new Set(["", host, `.${host}`, root, `.${root}`]).forEach((domain) => { 200 paths.forEach((path) => expireCookie(name, domain, path)); 201 }); 202 }); 203 if (denied("vimeo")) { 204 try { 205 Object.keys(sessionStorage).filter((key) => key.startsWith("holzfilm:vimeo-thumbnail:")).forEach((key) => sessionStorage.removeItem(key)); 206 } catch (_) {} 207 } 208 } 209 210 function serviceMarkup(name, title, copy, provider, privacyUrl) { 211 return `<article class="hf-consent-service"> 212 <div><h4>${title}</h4><p>${copy}<br>Anbieter: ${provider} · <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Datenschutz</a></p></div> 213 <label class="hf-consent-switch"><input type="checkbox" data-hf-service="${name}" aria-label="${title} erlauben"><span aria-hidden="true"></span></label> 214 </article>`; 215 } 216 217 const chevronMarkup = `<span class="hf-consent-chevron" aria-hidden="true"><svg viewBox="0 0 20 20"><path d="m6 8 4 4 4-4"/></svg></span>`; 218 219 function renderLayer() { 220 if (layer) return layer; 221 layer = document.createElement("dialog"); 222 layer.className = "hf-consent-layer"; 223 layer.hidden = true; 224 layer.setAttribute("aria-labelledby", "hf-consent-title"); 225 layer.innerHTML = `<section class="hf-consent-panel"> 226 <div class="hf-consent-view" data-hf-view="intro"> 227 <div class="hf-consent-intro"> 228 <div> 229 <p class="hf-consent-kicker">Datenschutz</p> 230 <h2 class="hf-consent-title" id="hf-consent-title">Cookies und externe Dienste</h2> 231 <p class="hf-consent-copy">Optionale Analyse-, Marketing- und Vimeo-Dienste laden wir nur mit Ihrer Einwilligung. Ohne Auswahl bleiben sie deaktiviert; die Website ist weiterhin nutzbar.</p> 232 </div> 233 <div class="hf-consent-actions"> 234 <button class="hf-consent-button hf-consent-button--primary" type="button" data-hf-action="accept">Alle akzeptieren</button> 235 <button class="hf-consent-button" type="button" data-hf-action="reject">Alle ablehnen</button> 236 <button class="hf-consent-button hf-consent-button--quiet" type="button" data-hf-action="details">Individuell einstellen</button> 237 </div> 238 <nav class="hf-consent-links" aria-label="Rechtliche Informationen"><a href="${legalUrl("impressum")}">Impressum</a><a href="${legalUrl("datenschutz")}">Datenschutz</a></nav> 239 </div> 240 </div>
241 <div class="hf-consent-view hf-consent-view--details" data-hf-view="details" hidden> 242 <header class="hf-consent-head"> 243 <div><p class="hf-consent-kicker">Datenschutzeinstellungen</p><h2 id="hf-consent-settings-title">Sie entscheiden.</h2><p>Alle optionalen Dienste sind standardmäÃig aus. Ihre Auswahl gilt sechs Monate und kann jederzeit über âCookie-Einstellungenâ im Footer geändert werden.</p></div> 244 <button class="hf-consent-button hf-consent-back" type="button" data-hf-action="back">Zurück</button> 245 </header> 246 <div class="hf-consent-categories"> 247 <details class="hf-consent-category"> 248 <summary>${chevronMarkup}<div><h3>Notwendig</h3><p>Speichert nur Ihre Consent-Auswahl lokal im Browser.</p></div><label class="hf-consent-switch"><input type="checkbox" checked disabled aria-label="Notwendige Speicherung aktiv"><span aria-hidden="true"></span></label></summary> 249 <div class="hf-consent-services"><article class="hf-consent-service"><div><h4>holzfilm Consent</h4><p>Zweck: Speichern und Nachweisen Ihrer Auswahl · Speicherort: dieses Gerät · Laufzeit: 6 Monate</p></div></article></div> 250 </details> 251 <details class="hf-consent-category" data-hf-category="media"> 252 <summary>${chevronMarkup}<div><h3>Externe Medien</h3><p>Vimeo-Player und Vorschaubilder unserer Filmproduktionen.</p></div><label class="hf-consent-switch"><input type="checkbox" data-hf-master="vimeo" aria-label="Externe Medien erlauben"><span aria-hidden="true"></span></label></summary> 253 <div class="hf-consent-services">${serviceMarkup("vimeo", "Vimeo", "Stellt Video-Player, Vorschaubilder und Wiedergabe bereit. Beim Laden werden insbesondere IP-Adresse sowie Geräte-, Player- und Nutzungsdaten in die USA übermittelt. Wir verwenden den Vimeo-Player mit DNT=1; dennoch können technisch erforderliche Sicherheits-Cookies gesetzt und bereits vorhandene Vimeo-Cookies übermittelt werden. Die Freigabe gilt sechs Monate und ist jederzeit widerrufbar.", "Vimeo.com, Inc. (USA)", "https://vimeo.com/legal/de/datenschutz/policy/")}</div> 254 </details> 255 <details class="hf-consent-category" data-hf-category="statistics"> 256 <summary>${chevronMarkup}<div><h3>Statistik</h3><p>Nutzungsanalyse und Optimierung der Website.</p></div><label class="hf-consent-switch"><input type="checkbox" data-hf-master="analytics,hotjar" aria-label="Statistikdienste erlauben"><span aria-hidden="true"></span></label></summary> 257 <div class="hf-consent-services"> 258 ${serviceMarkup("analytics", "Google Analytics", "Analyse von Seitenaufrufen, Interaktionen, Geräten und Kampagnen. Typische Cookies: _ga, _gid; Laufzeit je nach GTM-Konfiguration.", "Google Ireland Limited", "https://policies.google.com/privacy?hl=de")} 259 ${serviceMarkup("hotjar", "Hotjar", "Heatmaps, Session-Analyse und Feedback zur Verbesserung der Bedienbarkeit. Typische Cookies beginnen mit _hj.", "Hotjar Ltd.", "https://www.hotjar.com/legal/policies/privacy/")} 260 </div> 261 </details> 262 <details class="hf-consent-category" data-hf-category="marketing"> 263 <summary>${chevronMarkup}<div><h3>Marketing</h3><p>Conversion-Messung, Kampagnenoptimierung und B2B-Besucheridentifikation.</p></div><label class="hf-consent-switch"><input type="checkbox" data-hf-master="googleAds,facebook,linkedIn,rb2b" aria-label="Marketingdienste erlauben"><span aria-hidden="true"></span></label></summary> 264 <div class="hf-consent-services"> 265 ${serviceMarkup("googleAds", "Google Ads Conversion Tracking", "Ordnet Anfragen Anzeigenkontakten zu. Nach Ihrer Einwilligung speichern wir erste und letzte Anzeigenkontakte mit Zeitpunkt, Einstiegsseite, Kampagnenparametern und Klickkennung höchstens 90 Tage im Browser (holzfilm_ads_attribution_v1) und übermitteln sie mit Ihrer Anfrage an holzfilm. An Google senden wir ein Conversion-Ereignis mit zufälliger Anfrage-ID, keine Formularinhalte. Typische Cookies: _gcl_aw, _gcl_au.", "Google Ireland Limited", "https://policies.google.com/privacy?hl=de")} 266 ${serviceMarkup("facebook", "Meta Pixel", "Misst Conversions und bildet Zielgruppen für Werbung auf Facebook und Instagram. Typisches Cookie: _fbp.", "Meta Platforms Ireland Limited", "https://www.facebook.com/privacy/policy/")} 267 ${serviceMarkup("linkedIn", "LinkedIn Insight Tag", "B2B-Kampagnenmessung und Zielgruppenbildung auf LinkedIn. Typische Cookies: bcookie, lidc, UserMatchHistory.", "LinkedIn Ireland Unlimited Company", "https://www.linkedin.com/legal/privacy-policy")} 268 ${serviceMarkup("rb2b", "RB2B / Retention.com", "Identifiziert mögliche Unternehmensbesucher und kann Websiteaktivitäten mit geschäftlichen Kontaktdaten verknüpfen.", "GetEmails, LLC (USA)", "https://www.rb2b.com/privacy-policy")} 269 </div> 270 </details> 271 </div> 272 <nav class="hf-consent-links" aria-label="Rechtliche Informationen"><a href="${legalUrl("impressum")}">Impressum</a><a href="${legalUrl("datenschutz")}">Datenschutz</a></nav> 273 <div class="hf-consent-detail-actions"> 274 <button class="hf-consent-button hf-consent-button--primary" type="button" data-hf-action="save">Auswahl speichern</button> 275 <button class="hf-consent-button" type="button" data-hf-action="reject">Alle ablehnen</button> 276 <button class="hf-consent-button" type="button" data-hf-action="accept">Alle akzeptieren</button> 277 </div> 278 </div> 279 </section>`; 280 document.body.appendChild(layer); 281 bindLayerEvents(); 282 return layer; 283 } 284 285 function syncForm(choices = decision?.choices || blankChoices()) {
286 services.forEach((name) => { 287 const input = layer?.querySelector(`[data-hf-service="${name}"]`); 288 if (input) input.checked = Boolean(choices[name]); 289 }); 290 syncMasters(); 291 } 292 293 function syncMasters() { 294 layer?.querySelectorAll("[data-hf-master]").forEach((master) => { 295 const names = master.dataset.hfMaster.split(","); 296 const values = names.map((name) => Boolean(layer.querySelector(`[data-hf-service="${name}"]`)?.checked)); 297 master.checked = values.every(Boolean); 298 master.indeterminate = values.some(Boolean) && !values.every(Boolean); 299 }); 300 } 301 302 function showView(name) { 303 layer.querySelectorAll("[data-hf-view]").forEach((view) => { view.hidden = view.dataset.hfView !== name; }); 304 layer.setAttribute("aria-labelledby", name === "details" ? "hf-consent-settings-title" : "hf-consent-title"); 305 if (name === "details") { 306 syncForm(); 307 if (requestedService === "vimeo") layer.querySelector('[data-hf-category="media"]')?.setAttribute("open", ""); 308 } 309 } 310 311 function open(options = {}) { 312 renderLayer(); 313 requestedService = options.service || null; 314 if (!layer.contains(document.activeElement)) lastFocus = document.activeElement; 315 const details = options.details === true || (Boolean(decision) && options.details !== false); 316 showView(details ? "details" : "intro"); 317 if (layer.open) layer.close(); 318 layer.hidden = false; 319 layer.classList.toggle("hf-consent-layer--passive", !details); 320 layer.classList.toggle("hf-consent-layer--modal", details); 321 if (details) { 322 // Apply page styles before the native dialog performs layout and focus. 323 document.body.classList.add("modal-open"); 324 layer.showModal(); 325 window.setTimeout(() => layer.querySelector('[data-hf-view="details"] button')?.focus(), 0); 326 } else { 327 if (!document.querySelector("dialog[open]:not(.hf-consent-layer)")) document.body.classList.remove("modal-open"); 328 layer.show(); 329 } 330 } 331 332 function close() { 333 if (!layer) return; 334 if (layer.open) layer.close(); 335 layer.hidden = true; 336 if (!document.querySelector("dialog[open]")) document.body.classList.remove("modal-open"); 337 lastFocus?.focus?.(); 338 requestedService = null; 339 } 340 341 function commit(choices, source) { 342 const previous = decision?.choices || blankChoices(); 343 const next = normalizeChoices(choices); 344 clearKnownCookies(next); 345 applyDecision(writeDecision(next, source)); 346 close(); 347 const requiresCleanReload = services.some((name) => previous[name] && !next[name]); 348 if (requiresCleanReload) window.setTimeout(() => location.reload(), 40); 349 } 350 351 function bindLayerEvents() { 352 layer.addEventListener("cancel", (event) => event.preventDefault()); 353 layer.querySelectorAll("summary .hf-consent-switch").forEach((control) => { 354 control.addEventListener("click", (event) => event.stopPropagation()); 355 }); 356 layer.addEventListener("change", (event) => { 357 const master = event.target.closest("[data-hf-master]"); 358 if (master) { 359 master.dataset.hfMaster.split(",").forEach((name) => { 360 const input = layer.querySelector(`[data-hf-service="${name}"]`); 361 if (input) input.checked = master.checked; 362 }); 363 } 364 syncMasters(); 365 }); 366 layer.addEventListener("click", (event) => { 367 const action = event.target.closest("[data-hf-action]")?.dataset.hfAction; 368 if (!action) return; 369 if (action === "details") return open({ details: true }); 370 if (action === "back") return decision ? close() : open({ details: false }); 371 if (action === "accept") return commit(Object.fromEntries(services.map((name) => [name, true])), "accept-all"); 372 if (action === "reject") return commit(blankChoices(), "reject-all"); 373 if (action === "save") { 374 const choices = blankChoices(); 375 services.forEach((name) => { choices[name] = Boolean(layer.querySelector(`[data-hf-service="${name}"]`)?.checked); }); 376 return commit(choices, "custom"); 377 } 378 }); 379 layer.addEventListener("keydown", (event) => { 380 if (event.key !== "Tab" || !layer.classList.contains("hf-consent-layer
380--modal")) return; 381 const focusable = [...layer.querySelectorAll('button:not([disabled]), a[href], input:not([disabled])')].filter((element) => !element.closest("[hidden]")); 382 if (!focusable.length) return; 383 const first = focusable[0]; 384 const last = focusable[focusable.length - 1]; 385 if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } 386 else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } 387 }); 388 } 389 390 function addFooterControl() { 391 if (document.querySelector("[data-hf-open-consent]")) return; 392 const target = document.querySelector(".footer-bottom > div") || document.querySelector("footer"); 393 if (!target) return; 394 const button = document.createElement("button"); 395 button.type = "button"; 396 button.className = "hf-consent-reopen"; 397 button.dataset.hfOpenConsent = ""; 398 button.textContent = "Cookie-Einstellungen"; 399 button.addEventListener("click", () => open({ details: true })); 400 target.appendChild(button); 401 } 402 403 window.HolzfilmConsent = { 404 has, 405 open, 406 get: () => decision ? JSON.parse(JSON.stringify(decision)) : null, 407 allow(service) { 408 if (!services.includes(service)) return false; 409 if (has(service)) return true; 410 const choices = normalizeChoices(decision?.choices || blankChoices()); 411 choices[service] = true; 412 commit(choices, `allow-${service}`); 413 return true; 414 }, 415 request(service) { 416 if (has(service)) return Promise.resolve(true); 417 open({ details: true, service }); 418 return new Promise((resolve) => { 419 const handler = (event) => { 420 document.removeEventListener("holzfilm:consentchange", handler); 421 resolve(Boolean(event.detail?.choices?.[service])); 422 }; 423 document.addEventListener("holzfilm:consentchange", handler); 424 }); 425 } 426 }; 427 428 window.pushGoogleEvent = function (eventName, eventLabel, properties = {}) { 429 if (!has("analytics")) return false; 430 window.dataLayer.push({ 431 event: String(eventName || "click_btn"), 432 event_label: String(eventLabel || "unknown"), 433 ...properties 434 }); 435 return true; 436 }; 437 438 if (decision) { 439 clearKnownCookies(decision.choices); 440 applyDecision(decision); 441 } 442 443 // Apply withdrawals made in another tab before any later form submission. 444 window.addEventListener("storage", (event) => { 445 if (event.key !== STORAGE_KEY && event.key !== null) return; 446 const previous = decision?.choices || blankChoices(); 447 const nextDecision = readDecision(); 448 const next = nextDecision?.choices || blankChoices(); 449 clearKnownCookies(next); 450 decision = nextDecision; 451 updateGoogleConsent(next); 452 pushLegacyEvents(next); 453 document.dispatchEvent(new CustomEvent("holzfilm:consentchange", { detail: { choices: next } })); 454 if (services.some((name) => previous[name] && !next[name])) location.reload(); 455 else if (anyTracking(next)) loadGtm(); 456 }); 457 458 document.addEventListener("DOMContentLoaded", () => { 459 addFooterControl(); 460 document.addEventListener("click", (event) => { 461 const cta = event.target.closest("a.glass-button"); 462 if (!cta) return; 463 const segments = location.pathname.split("/").filter(Boolean); 464 const lastSegment = segments[segments.length - 1]; 465 const page = (lastSegment === "index.html" ? segments[segments.length - 2] : lastSegment) || "startseite"; 466 const label = cta.textContent.trim().toLowerCase().replace(/[^a-z0-9äöüÃ]+/gi, "_").replace(/^_|_$/g, "").slice(0, 64); 467 window.pushGoogleEvent("click_btn", `cta_${page}_${label}`, { 468 link_url: cta.href, 469 page_path: location.pathname 470 }); 471 }); 472 if (!decision) open(); 473 }); 474})();
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.