/** * AlpineConsent — public loader. Granular category consent, persisted, * Shadow-DOM isolated, themeable via --alpine-* props (docs/TECHNICAL.md). * Prior-blocks data-category scripts/iframes (incl. late-injected) and * signals Google Consent Mode v2. Layout/text/buttons/en-de language * read from window.AlpineConsentConfig when present (WordPress plugin's * class-loader.php), else fall back to the defaults below. NOT yet * included: theme-matching the host site, languages beyond en/de. * * Canonical source — wordpress-plugin/assets/js/alpineconsent.js should * eventually load a build of THIS file. */ (function () { 'use strict'; var STORAGE_KEY = 'alpineconsent_v1'; // gitleaks:allow var COOKIE_KEY = '__alpineconsent'; var CATEGORIES = ['preferences', 'statistics', 'marketing']; var IDLE_DISCONNECT_MS = 30000; function uuidv4() { if (window.crypto && window.crypto.randomUUID) { return window.crypto.randomUUID(); } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0; var v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function readStoredConsent() { try { var raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return null; var parsed = JSON.parse(raw); if (parsed && parsed.version === 1 && parsed.consent) return parsed; return null; } catch (e) { return null; } } function writeStoredConsent(consent) { var record = { version: 1, consent: consent, timestamp: new Date().toISOString(), id: uuidv4(), }; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(record)); } catch (e) {} try { var maxAge = 60 * 60 * 24 * 365; document.cookie = COOKIE_KEY + '=' + encodeURIComponent(JSON.stringify(record)) + '; path=/; max-age=' + maxAge + '; SameSite=Lax'; } catch (e) {} return record; } // dataLayer is per-pageload — a returning visitor's stored consent // still needs gtagUpdate() re-fired on every load, not just once. function gtagDefault() { window.dataLayer = window.dataLayer || []; window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; window.gtag('consent', 'default', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', analytics_storage: 'denied', wait_for_update: 500, }); } function gtagUpdate(consent) { if (!window.gtag) return; var granted = consent.marketing ? 'granted' : 'denied'; window.gtag('consent', 'update', { ad_storage: granted, ad_user_data: granted, ad_personalization: granted, analytics_storage: consent.statistics ? 'granted' : 'denied', }); } // Copies every attribute but `type` (data-src -> src), so // async/defer/etc. survive the restore. function loadScript(node) { var script = document.createElement('script'); for (var i = 0; i < node.attributes.length; i++) { var attr = node.attributes[i]; if (attr.name === 'type') continue; if (attr.name === 'data-src') { script.src = attr.value; continue; } script.setAttribute(attr.name, attr.value); } if (!script.src && node.textContent) { script.textContent = node.textContent; } if (node.parentNode) node.parentNode.replaceChild(script, node); return script; } function unblockIframe(node) { var src = node.getAttribute('data-src'); if (src) node.src = src; } function applyConsentToNode(node, consent) { var category = node.getAttribute('data-category'); if (!category || !consent[category]) return; if (node.tagName === 'SCRIPT' && node.getAttribute('type') === 'text/plain') { loadScript(node); } else if (node.tagName === 'IFRAME' && node.getAttribute('data-src')) { unblockIframe(node); } } function applyConsent(consent) { var nodes = document.querySelectorAll('script[type="text/plain"][data-category], iframe[data-category]'); for (var i = 0; i < nodes.length; i++) applyConsentToNode(nodes[i], consent); } // Idle-disconnects after 30s (cost control on heavy SPAs), re-arms // on the next interaction. function watchForLateInjections() { var idleTimer; var observer = new MutationObserver(function (mutations) { var consent = readStoredConsent(); if (!consent) return; for (var m = 0; m < mutations.length; m++) { var added = mutations[m].addedNodes; for (var n = 0; n < added.length; n++) { var node = added[n]; if (node.nodeType !== 1) continue; if (node.hasAttribute && node.hasAttribute('data-category')) { applyConsentToNode(node, consent.consent); } if (node.querySelectorAll) { var nested = node.querySelectorAll('[data-category]'); for (var k = 0; k < nested.length; k++) applyConsentToNode(nested[k], consent.consent); } } } }); function arm() { observer.observe(document.documentElement, { childList: true, subtree: true, attributeFilter: ['src', 'data-src', 'data-category'], }); clearTimeout(idleTimer); idleTimer = setTimeout(function () { observer.disconnect(); }, IDLE_DISCONNECT_MS); } arm(); ['click', 'keydown', 'scroll'].forEach(function (evt) { document.addEventListener(evt, arm, { passive: true }); }); } var LAYOUTS = ['modal', 'bar-top', 'bar-bottom', 'floating']; var DEFAULT_BUTTONS = [ { action: 'reject', style: 'button' }, { action: 'manage', style: 'button' }, { action: 'accept', style: 'button' }, ]; var STRINGS = { en: { title: 'We use cookies', description: 'We use cookies for essential functions and, with consent, for preferences, statistics, and marketing.', accept: 'Accept all', reject: 'Reject all', manage: 'Manage preferences', save: 'Save preferences', necessary: 'Necessary (always on)', preferences: 'Preferences', statistics: 'Statistics', marketing: 'Marketing', poweredBy: 'Powered by AlpineConsent', }, de: { title: 'Wir verwenden Cookies', description: 'Wir verwenden Cookies für wesentliche Funktionen und, mit Zustimmung, für Präferenzen, Statistiken und Marketing.', accept: 'Alle akzeptieren', reject: 'Alle ablehnen', manage: 'Einstellungen verwalten', save: 'Einstellungen speichern', necessary: 'Notwendig (immer aktiv)', preferences: 'Präferenzen', statistics: 'Statistiken', marketing: 'Marketing', poweredBy: 'Bereitgestellt von AlpineConsent', }, }; function getConfig() { var raw = (typeof window.AlpineConsentConfig === 'object' && window.AlpineConsentConfig) || {}; var buttons = Array.isArray(raw.buttons) && raw.buttons.length ? raw.buttons : DEFAULT_BUTTONS; return { layout: LAYOUTS.indexOf(raw.layout) !== -1 ? raw.layout : 'modal', lang: raw.lang === 'de' ? 'de' : 'en', title: typeof raw.title === 'string' ? raw.title : '', description: typeof raw.description === 'string' ? raw.description : '', buttons: buttons, }; } var STYLE = ':host{all:initial}' + '.alpine-backdrop{position:fixed;inset:0;z-index:2147483000;display:flex;font-family:var(--alpine-font-family,system-ui,sans-serif)}' + '.alpine-modal{align-items:flex-end;justify-content:center;background:rgba(0,0,0,.5)}' + '.alpine-bar-top{align-items:flex-start;justify-content:stretch}' + '.alpine-bar-bottom{align-items:flex-end;justify-content:stretch}' + '.alpine-floating{align-items:flex-end;justify-content:flex-end}' + '.alpine-banner{background:var(--alpine-bg,#fff);color:var(--alpine-text,#333);border-radius:var(--alpine-border-radius,8px);box-shadow:0 4px 24px rgba(0,0,0,.2);padding:20px}' + '.alpine-modal .alpine-banner,.alpine-floating .alpine-banner{width:100%;max-width:640px;margin:0 16px 16px}' + '.alpine-floating .alpine-banner{max-width:360px}' + '.alpine-bar-top .alpine-banner,.alpine-bar-bottom .alpine-banner{width:100%;margin:0;border-radius:0;display:flex;align-items:center;gap:16px;flex-wrap:wrap}' + '.alpine-bar-top .alpine-text,.alpine-bar-bottom .alpine-text{flex:1 1 240px}' + '.alpine-banner h2{margin:0 0 8px;font-size:16px}' + '.alpine-banner p{margin:0;font-size:14px;line-height:1.5}' + '.alpine-prefs{margin:16px 0;display:grid;gap:8px;width:100%}' + '.alpine-prefs[hidden]{display:none}' + '.alpine-prefs label{display:flex;align-items:center;gap:8px;font-size:13px}' + '.alpine-prefs input:focus-visible{outline:2px solid var(--alpine-primary,#0073aa);outline-offset:2px}' + '.alpine-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px}' + '.alpine-bar-top .alpine-actions,.alpine-bar-bottom .alpine-actions{margin-top:0}' + '.alpine-btn{flex:1 1 auto;min-width:120px;padding:10px 16px;border-radius:var(--alpine-border-radius,8px);border:2px solid transparent;font-size:14px;font-weight:600;cursor:pointer;background:transparent;color:var(--alpine-text,#333);text-decoration:none;text-align:center;display:inline-block;box-sizing:border-box}' + '.alpine-btn[hidden]{display:none}' + '.alpine-btn:focus-visible{outline:2px solid var(--alpine-primary,#0073aa);outline-offset:2px}' + '.alpine-btn-accept{background:var(--alpine-btn-accept-bg,#10b981);color:#fff}' + '.alpine-btn-reject,.alpine-btn-manage,.alpine-btn-save,.alpine-btn-link{border-color:var(--alpine-primary,#0073aa)}' + '.alpine-link{flex:0 0 auto;min-width:0;border-color:transparent;padding:10px 4px;text-decoration:underline;font-weight:400}' + '.alpine-badge{display:block;margin-top:12px;font-size:11px;text-align:center;color:var(--alpine-text,#333);opacity:.6;text-decoration:none}' + '.alpine-bar-top .alpine-badge,.alpine-bar-bottom .alpine-badge{flex:0 0 100%;margin-top:0}'; function createButton(strings, cfg, handlers) { var asLink = cfg.style === 'link'; var el = document.createElement(asLink ? 'a' : 'button'); if (asLink) { el.href = cfg.action === 'link' ? cfg.url : '#'; if (cfg.action === 'link') { el.target = '_blank'; el.rel = 'noopener noreferrer'; } } else { el.type = 'button'; } el.className = 'alpine-btn alpine-btn-' + cfg.action + (asLink ? ' alpine-link' : ''); el.textContent = cfg.action === 'link' ? cfg.label || '' : strings[cfg.action] || cfg.action; var handler = handlers[cfg.action]; if (handler) { el.addEventListener('click', function (e) { if (asLink) e.preventDefault(); handler(el); }); } return el; } function renderBanner(onDecision) { var config = getConfig(); var strings = STRINGS[config.lang]; var host = document.createElement('div'); host.id = 'alpine-consent-root'; var shadow = host.attachShadow({ mode: 'open' }); var style = document.createElement('style'); style.textContent = STYLE; shadow.appendChild(style); var backdrop = document.createElement('div'); backdrop.className = 'alpine-backdrop alpine-' + config.layout; var banner = document.createElement('div'); banner.className = 'alpine-banner'; banner.setAttribute('role', 'dialog'); banner.setAttribute('aria-modal', 'true'); banner.setAttribute('aria-live', 'polite'); banner.setAttribute('aria-labelledby', 'alpine-consent-title'); banner.setAttribute('aria-describedby', 'alpine-consent-desc'); var textWrap = document.createElement('div'); textWrap.className = 'alpine-text'; var title = document.createElement('h2'); title.id = 'alpine-consent-title'; title.textContent = config.title || strings.title; var desc = document.createElement('p'); desc.id = 'alpine-consent-desc'; desc.textContent = config.description || strings.description; textWrap.appendChild(title); textWrap.appendChild(desc); var prefs = document.createElement('div'); prefs.className = 'alpine-prefs'; prefs.hidden = true; var checkboxes = {}; function addRow(label, category, checked, disabled) { var row = document.createElement('label'); var box = document.createElement('input'); box.type = 'checkbox'; box.checked = checked; box.disabled = !!disabled; if (category) checkboxes[category] = box; row.appendChild(box); row.appendChild(document.createTextNode(label)); prefs.appendChild(row); } addRow(strings.necessary, null, true, true); addRow(strings.preferences, 'preferences', false, false); addRow(strings.statistics, 'statistics', false, false); addRow(strings.marketing, 'marketing', false, false); var actions = document.createElement('div'); actions.className = 'alpine-actions'; var saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'alpine-btn alpine-btn-save'; saveBtn.textContent = strings.save; saveBtn.hidden = true; saveBtn.addEventListener('click', function () { var consent = { necessary: true }; CATEGORIES.forEach(function (cat) { consent[cat] = checkboxes[cat].checked; }); finish(consent); }); function finish(consent) { var record = writeStoredConsent(consent); gtagUpdate(consent); applyConsent(consent); host.remove(); onDecision(record); } var handlers = { accept: function () { finish({ necessary: true, preferences: true, statistics: true, marketing: true }); }, reject: function () { finish({ necessary: true, preferences: false, statistics: false, marketing: false }); }, manage: function (el) { var expanded = !prefs.hidden; prefs.hidden = expanded; saveBtn.hidden = expanded; el.setAttribute('aria-expanded', String(!expanded)); }, }; var firstBtn = null; config.buttons.forEach(function (btnConfig) { var el = createButton(strings, btnConfig, handlers); if (btnConfig.action === 'manage') el.setAttribute('aria-expanded', 'false'); if (!firstBtn) firstBtn = el; actions.appendChild(el); }); var badge = document.createElement('a'); badge.className = 'alpine-badge'; badge.href = 'https://alpineconsent.com'; badge.target = '_blank'; badge.rel = 'noopener noreferrer'; badge.textContent = strings.poweredBy; banner.appendChild(textWrap); banner.appendChild(prefs); banner.appendChild(actions); banner.appendChild(saveBtn); banner.appendChild(badge); backdrop.appendChild(banner); shadow.appendChild(backdrop); document.body.appendChild(host); if (firstBtn) { firstBtn.focus(); } else { banner.setAttribute('tabindex', '-1'); banner.focus(); } } function init() { var api = (window.AlpineConsent = window.AlpineConsent || {}); api.version = '0.4.0'; api.getConsent = function () { var stored = readStoredConsent(); return stored ? stored.consent : null; }; api.showBanner = function (onDecision) { renderBanner(onDecision || function () {}); }; gtagDefault(); watchForLateInjections(); var stored = readStoredConsent(); if (stored) { gtagUpdate(stored.consent); applyConsent(stored.consent); } else { api.showBanner(function (record) { document.dispatchEvent(new CustomEvent('alpineConsentUpdated', { detail: record })); }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();