1/*! 2 * PURA Cookie Consent Widget â standalone, self-hosted, zero-dependency. 3 * RGPD / LSSI / AEPD 2024 compliant. Google Consent Mode v2. 4 * 5 * SKELETON / STARTER. Built from the validated mockup (mockup-cookie-consent.html) 6 * and the PRD (cookie-consent-widget-PRD.md). Read IMPLEMENTATION-GUIDE.md before shipping. 7 * 8 * Embed on each money site, in <head>, BEFORE any tracking script: 9 * <script src="https://<host>/cookies.js" 10 * data-privacy-url="/politica-de-cookies/" 11 * data-accent-color="#8B6F4E"></script> 12 * 13 * Tag every tracking <script> so it stays blocked until consent: 14 *
vendor: 123 bytes, lines 14-15
14 <script type="text/plain" data-cookie-category="analytics" 15 * data-src="https://www.googletagmanager.com/gtag/js?id=
15G-XXXX
vendor: 13 bytes, lines 15-16
15"></script> 16
16* 17 * Per-site override (optional): define window.ocCookieConfig BEFORE this script 18 * to replace categories / cookie lists / copy. See DEFAULTS below for the shape. 19 */ 20(function () { 21 'use strict'; 22 23 if (window.__ocCookieWidgetLoaded) { return; } 24 window.__ocCookieWidgetLoaded = true; 25 26 /* ------------------------------------------------------------------ * 27 * 0. Config â read <script> data-* attributes + window.ocCookieConfig 28 * ------------------------------------------------------------------ */ 29 var SCRIPT = document.currentScript || (function () { 30 var s = document.getElementsByTagName('script'); 31 return s[s.length - 1]; 32 })(); 33 34 function attr(name, fallback) { 35 var v = SCRIPT && SCRIPT.getAttribute(name); 36 return (v === null || v === undefined || v === '') ? fallback : v; 37 } 38 39 var override = window.ocCookieConfig || {}; 40 41 var CONFIG = { 42 privacyUrl: override.privacyUrl || attr('data-privacy-url', '/politica-de-cookies/'), 43 lang: override.lang || attr('data-lang', 'es'), 44 position: override.position || attr('data-position', 'bottom'), // bottom | bottom-left 45 theme: override.theme || attr('data-theme', 'light'), // light | dark (light only in skeleton) 46 accent: override.accent || attr('data-accent-color', '#8B6F4E'), 47 gtmId: override.gtmId || attr('data-gtm-id', null), 48 logEndpoint: override.logEndpoint || attr('data-log-endpoint', null), 49 expiryDays: parseInt(override.expiryDays || attr('data-expiry-days', '180'), 10), 50 cookieName: 'oc_cookie_consent', 51 storageKey: 'oc_cookie_consent_detail', 52 version: '1.0' 53 }; 54 55 /* ------------------------------------------------------------------ * 56 * 1. Categories â defaults mirror the PRD. Override per site via 57 * window.ocCookieConfig.categories. `locked` = always on (necessary). 58 * ------------------------------------------------------------------ */ 59 var DEFAULTS = { 60 es: { 61 bannerTitle: 'Utilizamos cookies', 62 bannerText: 'Usamos cookies propias y de terceros para analizar el tráfico y personalizar contenido. Puedes aceptar todas, rechazarlas o configurar tus preferencias.', 63 policyLink: 'PolÃtica de cookies', 64 reject: 'Rechazar', accept: 'Aceptar', config: 'Configurar', save: 'Guardar selección', 65 prefsTitle: 'Preferencias de cookies', 66 prefsSubtitle: 'Elige qué cookies permites. Puedes cambiar tus preferencias en cualquier momento.', 67 notice: 'Tus preferencias se guardan durante {DAYS} dÃas. Pasado ese tiempo, te volveremos a preguntar. Puedes cambiarlas en cualquier momento pulsando el icono ðª en la esquina inferior izquierda.', 68 cookiesLabel: 'cookies', cookieLabel: 'cookie', close: 'Cerrar', reopenTitle: 'Configurar cookies' 69 }, 70 en: { 71 bannerTitle: 'We use cookies', 72 bannerText: 'We use our own and third-party cookies to analyse traffic and personalise content. You can accept all, reject them or configure your preferences.', 73 policyLink: 'Cookie policy', 74 reject: 'Reject', accept: 'Accept', config: 'Configure', save: 'Save selection', 75 prefsTitle: 'Cookie preferences', 76 prefsSubtitle: 'Choose which cookies you allow. You can change your preferences at any time.', 77 notice: 'Your preferences are stored for {DAYS} days. After that, we will ask again. You can change them anytime via the ðª icon in the bottom-left corner.', 78 cookiesLabel: 'cookies', cookieLabel: 'cookie', close: 'Close', reopenTitle: 'Configure cookies' 79 } 80 }; 81 82 var T = DEFAULTS[CONFIG.lang] || DEFAULTS.es; 83 if (override.copy) { for (var k in override.copy) { T[k] = override.copy[k]; } } 84 85 // Default category definitions (from the PRD cookie audit). Edit per site. 86 var CATEGORIES = override.categories || [ 87 { 88 key: 'necessary', name: (CONFIG.lang === 'en' ? 'Necessary' : 'Necesarias'), 89 locked: true, def: true, 90 desc: (CONFIG.lang === 'en' 91 ? 'Essential for the site to work. Cannot be disabled.' 92 : 'Imprescindibles para el funcionamiento del sitio. No se pueden desactivar.'), 93 cookies: [ 94 { name: 'pll_language', provider: 'Polylang â Idioma' }, 95 { name: 'oc_cookie_consent', provider: 'Propio â Preferencias guardadas' } 96 ] 97 }, 98 { 99 key: 'analytics', name: (CONFIG.lang === 'en' ? 'Analytics' : 'AnalÃticas'),
100 locked: false, def: false, 101 desc: (CONFIG.lang === 'en' 102 ? 'Help us understand how visitors use the site.' 103 : 'Nos ayudan a entender cómo usan los visitantes el sitio web.'), 104 cookies: [ 105 { name: '_ga', provider: 'Google Analytics â ID de usuario' }, 106 { name: '_ga_*', provider: 'Google Analytics â Estado de sesión' }, 107 { name: '_ym_uid', provider: 'Yandex Metrika â ID de usuario' }, 108 { name: '_ym_d', provider: 'Yandex Metrika â Primera visita' }, 109 { name: '_ym_isad', provider: 'Yandex Metrika â Detección ad-blocker' } 110 ] 111 }, 112 { 113 key: 'marketing', name: 'Marketing', 114 locked: false, def: false, 115 desc: (CONFIG.lang === 'en' 116 ? 'Allow us to show personalised content and ads.' 117 : 'Permiten mostrarte contenido y anuncios personalizados.'), 118 cookies: [ 119 { name: '_fbp', provider: 'Meta Pixel â Conversiones' }, 120 { name: 'sib_cuid', provider: 'Brevo â ID de contacto email' }, 121 { name: 'ml_*', provider: 'MailerLite â Formularios email' } 122 ] 123 } 124 ]; 125 126 /* ------------------------------------------------------------------ * 127 * 2. Google Consent Mode v2 â DEFAULT DENY (must run before gtag.js). 128 * Tracking scripts are text/plain (blocked) until consent, so this 129 * runs first and Google honours it. 130 * ------------------------------------------------------------------ */ 131 window.dataLayer = window.dataLayer || []; 132 function gtag() { window.dataLayer.push(arguments); } 133 window.gtag = window.gtag || gtag; 134 gtag('consent', 'default', { 135 analytics_storage: 'denied', 136 ad_storage: 'denied', 137 ad_user_data: 'denied', 138 ad_personalization: 'denied', 139 wait_for_update: 500 140 }); 141 142 function updateConsentMode(state) { 143 gtag('consent', 'update', { 144 analytics_storage: state.analytics ? 'granted' : 'denied', 145 ad_storage: state.marketing ? 'granted' : 'denied', 146 ad_user_data: state.marketing ? 'granted' : 'denied', 147 ad_personalization: state.marketing ? 'granted' : 'denied' 148 }); 149 } 150 151 /* ------------------------------------------------------------------ * 152 * 3. Consent storage â cookie (180d) + localStorage mirror 153 * ------------------------------------------------------------------ */ 154 function readConsent() { 155 try { 156 var m = document.cookie.match(new RegExp('(?:^|; )' + CONFIG.cookieName + '=([^;]*)')); 157 if (m) { 158 var data = JSON.parse(decodeURIComponent(m[1])); 159 if (data && data.categories) { return data; } 160 } 161 } catch (e) {} 162 try { 163 var ls = localStorage.getItem(CONFIG.storageKey); 164 if (ls) { return JSON.parse(ls); } 165 } catch (e) {} 166 return null; 167 } 168 169 function writeConsent(categories) { 170 var record = { 171 version: CONFIG.version, 172 categories: categories, 173 timestamp: new Date().toISOString(), 174 expiry: CONFIG.expiryDays 175 }; 176 var encoded = encodeURIComponent(JSON.stringify(record)); 177 var maxAge = CONFIG.expiryDays * 24 * 60 * 60; 178 document.cookie = CONFIG.cookieName + '=' + encoded + 179 '; path=/; max-age=' + maxAge + '; SameSite=Lax' + 180 (location.protocol === 'https:' ? '; Secure' : ''); 181 try { localStorage.setItem(CONFIG.storageKey, JSON.stringify(record)); } catch (e) {} 182 return record; 183 } 184 185 /* ------------------------------------------------------------------ * 186 * 4. Apply consent â Consent Mode update + activate tagged scripts 187 * <script type="text/plain" data-cookie-category="analytics" 188 * data-src="..."> (or inline content) â real <script> 189 * ------------------------------------------------------------------ */ 190 function activateScripts(state) { 191 var blocked = document.querySelectorAll('script[type="text/plain"][data-cookie-category]'); 192 for (var i = 0; i < blocked.length; i++) { 193 var node = blocked[i]; 194 var cat = node.getAttribute('data-cookie-category'); 195 if (cat === 'necessary' || state[cat] !== true) { continue; } 196 if (node.getAttribute('data-oc-activated') === '1') { continue; } 197 198 var s = document.createElement('script'); 199 // Copy non-control attributes (id, async, defer, data-* of the vendor, etc.) 200 for (var a = 0; a < node.attributes.length; a++) { 201 var at = node.attributes[a]; 202 if (['type', 'data-src', 'data-cookie-category', 'data-oc-activated'].indexOf(at.name) !== -1) { continue; } 203 s.setAttribute(at.name, at.value); 204 } 205 s.type = 'text/javascript'; 206 var src = node.getAttribute('data-src'); 207 if (src) { s.src = src; } else { s.text = node.textContent || ''; } 208 node.setAttribute('data-oc-activated', '1'); 209 node.parentNode.insertBefore(s, node.nextSibling); 210 } 211 212 // Gated iframes / embeds (e.g. Google Maps): data-src -> src on consent. 213 var frames = document.querySelectorAll('iframe[data-cookie-category][data-src]'); 214 for (var f = 0; f < frames.length; f++) { 215 var fr = frames[f]; 216 var fcat = fr.getAttribute('data-cookie-category'); 217 if (fcat === 'necessary' || state[fcat] !== true) { continue; } 218 if (fr.getAttribute('data-oc-activated') === '1') { continue; } 219 fr.src = fr.getAttribute('data-src'); 220 fr.setAttribute('data-oc-activated', '1'); 221 fr.style.display = ''; 222 var ph = fr.previousElementSibling; 223 if (ph && ph.classList && ph.classList.contains('oc-embed-block')) { ph.parentNode.removeChild(ph); } 224 } 225 } 226 227 // Show a placeholder over blocked embeds so there is no empty hole, with a 228 // button that opens the banner to grant consent. 229 function markBlockedEmbeds() { 230 var frames = document.querySelectorAll('iframe[data-cookie-category][data-src]:not([data-oc-activated="1"])'); 231 for (var i = 0; i < frames.length; i++) { 232 var fr = frames[i]; 233 if (fr.previousElementSibling && fr.previousElementSibling.classList && 234 fr.previousElementSibling.classList.contains('oc-embed-block')) { continue; } 235 fr.style.display = 'none'; 236 var box = document.createElement('div'); 237 box.className = 'oc-embed-block'; 238 var h = fr.getAttribute('height'); 239 if (h && /^\d+$/.test(h)) { box.style.minHeight = h + 'px'; } 240 box.innerHTML = '<div class="oc-embed-inner"><p>' + 241 esc(T.embedBlocked || 'Este contenido se carga desde un servicio externo y requiere tu consentimiento.') + 242 '</p><button type="button" class="oc-btn oc-btn-accept" data-oc="config">' + 243 esc(T.embedButton || 'Configurar y cargar') + '</button></div>'; 244 fr.parentNode.insertBefore(box, fr); 245 box.addEventListener('click', function (e) { 246 if (e.target.closest('[data-oc="config"]')) { openPrefs(); } 247 }); 248 } 249 } 250 251 function applyConsent(state, persist) { 252 updateConsentMode(state); 253 activateScripts(state); 254 if (persist) { 255 var record = writeConsent(state); 256 logConsentServerSide(record); 257 } 258 } 259 260 /* Optional server-side proof (RGPD Art. 7.1). No-op if no endpoint set. */ 261 function logConsentServerSide(record) { 262 if (!CONFIG.logEndpoint) { return; } 263 try { 264 var payload = JSON.stringify({ 265 domain: location.hostname, 266 categories: record.categories, 267 user_agent: navigator.userAgent, 268 timestamp: record.timestamp 269 }); 270 if (navigator.sendBeacon) { 271 navigator.sendBeacon(CONFIG.logEndpoint, new Blob([payload], { type: 'application/json' })); 272 } else { 273 fetch(CONFIG.logEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true }); 274 } 275 } catch (e) {} 276 } 277 278 /* ------------------------------------------------------------------ * 279 * 5. Choice handlers 280 * ------------------------------------------------------------------ */ 281 function allCategories(value) { 282 var out = {}; 283 CATEGORIES.forEach(function (c) { out[c.key] = c.locked ? true : value; }); 284 return out; 285 } 286
287 function acceptAll() { applyConsent(allCategories(true), true); hideBanner(); closePrefs(); } 288 function rejectAll() { applyConsent(allCategories(false), true); hideBanner(); closePrefs(); } 289 290 function savePrefs() { 291 var state = {}; 292 CATEGORIES.forEach(function (c) { 293 if (c.locked) { state[c.key] = true; return; } 294 var input = document.getElementById('oc-cat-' + c.key); 295 state[c.key] = !!(input && input.checked); 296 }); 297 applyConsent(state, true); 298 hideBanner(); 299 closePrefs(); 300 } 301 302 /* ------------------------------------------------------------------ * 303 * 6. UI â inject CSS + DOM (mirrors the validated mockup) 304 * ------------------------------------------------------------------ */ 305 var ACCENT = CONFIG.accent; 306 var ACCENT_DARK = shade(ACCENT, -0.12); 307 308 function shade(hex, pct) { 309 try { 310 var n = parseInt(hex.replace('#', ''), 16); 311 var r = Math.max(0, Math.min(255, (n >> 16) + Math.round(255 * pct))); 312 var g = Math.max(0, Math.min(255, ((n >> 8) & 0xff) + Math.round(255 * pct))); 313 var b = Math.max(0, Math.min(255, (n & 0xff) + Math.round(255 * pct))); 314 return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 315 } catch (e) { return hex; } 316 } 317 318 var CSS = [ 319 '.oc-cookie-banner{position:fixed;bottom:0;left:0;right:0;z-index:99999;background:#fff;border-top:1px solid #e5e0db;box-shadow:0 -4px 30px rgba(0,0,0,.08);padding:20px 32px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;transform:translateY(0);transition:transform .4s cubic-bezier(.16,1,.3,1)}', 320 '.oc-cookie-banner.oc-hidden{transform:translateY(100%)}', 321 '.oc-cookie-inner{max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:28px}', 322 '.oc-cookie-text{flex:1}', 323 '.oc-cookie-text h3{font-size:15px;font-weight:600;color:#1a1a1a;margin:0 0 4px}', 324 '.oc-cookie-text p{font-size:13px;line-height:1.5;color:#666;margin:0}', 325 '.oc-cookie-text a{color:' + ACCENT + ';text-decoration:underline}', 326 '.oc-cookie-actions{display:flex;gap:10px;flex-shrink:0;align-items:center}', 327 '.oc-btn{padding:11px 22px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .2s;white-space:nowrap;text-align:center;font-family:inherit}', 328 '.oc-btn:hover{transform:translateY(-1px)}', 329 '.oc-btn-accept{background:' + ACCENT + ';color:#fff}.oc-btn-accept:hover{background:' + ACCENT_DARK + '}', 330 '.oc-btn-reject{background:#4a4a4a;color:#fff}.oc-btn-reject:hover{background:#333}', 331 '.oc-btn-save{background:' + ACCENT + ';color:#fff;opacity:.7}.oc-btn-save:hover{opacity:1}', 332 '.oc-btn-config{background:transparent;color:' + ACCENT + ';padding:11px 14px;text-decoration:underline;font-weight:500}', 333 '.oc-cookie-prefs{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);opacity:0;pointer-events:none;transition:opacity .3s}', 334 '.oc-cookie-prefs.oc-visible{opacity:1;pointer-events:auto}', 335 '.oc-prefs-card{background:#fff;border-radius:16px;width:520px;max-width:95vw;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.2);transform:translateY(20px);transition:transform .3s cubic-bezier(.16,1,.3,1);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}', 336 '.oc-cookie-prefs.oc-visible .oc-prefs-card{transform:translateY(0)}', 337 '.oc-prefs-header{padding:24px 28px 16px;border-bottom:1px solid #f0ebe6;display:flex;justify-content:space-between;align-items:flex-start}', 338 '.oc-prefs-header h2{font-size:18px;font-weight:700;color:#1a1a1a;margin:0 0 4px}', 339 '.oc-prefs-header p{font-size:13px;color:#888;margin:0}', 340 '.oc-prefs-close{width:32px;height:32px;border:none;background:#f5f0eb;border-radius:8px;cursor:pointer;font-size:16px;color:#999;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:12px;transition:background .15s}', 341 '.oc-prefs-close:hover{background:#ebe4dc;color:#666}', 342 '.oc-prefs-body{padding:8px 28px 16px}', 343 '.oc-cat{border-bottom:1px solid #f5f0eb;padding:16px 0}.oc-cat:last-child{border-bottom:none}', 344 '.oc-cat-header{display:flex;align-items:center;justify-content:space-between;cursor:pointer}', 345 '.oc-cat-info{flex:1}', 346 '.oc-cat-info h4{font-size:14px;font-weight:600;
346color:#1a1a1a;display:flex;align-items:center;gap:8px;margin:0}', 347 '.oc-cat-info h4 .oc-count{font-size:11px;font-weight:500;color:#999;background:#f5f0eb;padding:1px 7px;border-radius:10px}', 348 '.oc-cat-info p{font-size:12px;color:#888;margin:3px 0 0;line-height:1.4}', 349 '.oc-toggle{position:relative;width:44px;height:24px;flex-shrink:0;margin-left:16px}', 350 '.oc-toggle input{opacity:0;width:0;height:0;position:absolute}', 351 '.oc-toggle .oc-slider{position:absolute;inset:0;background:#ddd;border-radius:24px;cursor:pointer;transition:background .2s}', 352 '.oc-toggle .oc-slider::before{content:"";position:absolute;width:18px;height:18px;background:#fff;border-radius:50%;top:3px;left:3px;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.15)}', 353 '.oc-toggle input:checked + .oc-slider{background:' + ACCENT + '}', 354 '.oc-toggle input:checked + .oc-slider::before{transform:translateX(20px)}', 355 '.oc-toggle input:disabled + .oc-slider{background:#c4b5a3;cursor:not-allowed}', 356 '.oc-cat-detail{max-height:0;overflow:hidden;transition:max-height .3s ease}', 357 '.oc-cat.oc-expanded .oc-cat-detail{max-height:400px}', 358 '.oc-cat-header .oc-arrow{font-size:10px;color:#bbb;margin-right:8px;transition:transform .2s}', 359 '.oc-cat.oc-expanded .oc-cat-header .oc-arrow{transform:rotate(90deg)}', 360 '.oc-cookie-list{margin-top:10px;background:#faf8f5;border-radius:8px;padding:10px 14px}', 361 '.oc-cookie-item{display:flex;justify-content:space-between;padding:5px 0;font-size:12px;color:#666;border-bottom:1px solid #f0ebe6;gap:12px}', 362 '.oc-cookie-item:last-child{border-bottom:none}', 363 '.oc-cookie-item .oc-name{font-weight:600;color:#555;font-family:monospace;font-size:11px}', 364 '.oc-cookie-item .oc-provider{color:#999;text-align:right}', 365 '.oc-prefs-notice{padding:0 28px 16px;font-size:11px;color:#aaa;line-height:1.4}', 366 '.oc-prefs-footer{padding:16px 28px 24px;display:flex;gap:10px;border-top:1px solid #f0ebe6}', 367 '.oc-prefs-footer .oc-btn{flex:1}', 368 '.oc-cookie-reopen{position:fixed;bottom:20px;left:20px;z-index:99990;width:40px;height:40px;border-radius:50%;background:#fff;border:1px solid #e5e0db;box-shadow:0 2px 12px rgba(0,0,0,.1);cursor:pointer;display:none;align-items:center;justify-content:center;font-size:18px;transition:transform .2s}', 369 '.oc-cookie-reopen:hover{transform:scale(1.1)}.oc-cookie-reopen.oc-visible{display:flex}', 370 '@media(max-width:640px){.oc-cookie-inner{flex-direction:column;gap:14px;text-align:center}.oc-cookie-actions{width:100%;flex-direction:column}.oc-btn{width:100%}.oc-prefs-card{border-radius:16px 16px 0 0}}' 371 ].join(''); 372 373 var el = {}; 374 375 function build(stored) { 376 var style = document.createElement('style'); 377 style.id = 'oc-cookie-style'; 378 style.textContent = CSS; 379 document.head.appendChild(style); 380 381 var root = document.createElement('div'); 382 root.id = 'oc-cookie-root'; 383 384 // Banner 385 var pos = CONFIG.position === 'bottom-left' ? 'left:20px;right:auto;max-width:520px;' : ''; 386 root.innerHTML = 387 '<div class="oc-cookie-banner oc-hidden" id="oc-banner" style="' + pos + '">' + 388 '<div class="oc-cookie-inner">' + 389 '<div class="oc-cookie-text">' + 390 '<h3>' + esc(T.bannerTitle) + '</h3>' + 391 '<p>' + esc(T.bannerText) + ' ' + 392 '<a href="' + esc(CONFIG.privacyUrl) + '" target="_blank" rel="noopener">' + esc(T.policyLink) + '</a>' + 393 '</p>' + 394 '</div>' + 395 '<div class="oc-cookie-actions">' + 396 '<button class="oc-btn oc-btn-reject" data-oc="reject">' + esc(T.reject) + '</button>' + 397 '<button class="oc-btn oc-btn-accept" data-oc="accept">' + esc(T.accept) + '</button>' + 398 '<button class="oc-btn oc-btn-config" data-oc="config">' + esc(T.config) + '</button>' + 399 '</div>' + 400 '</div>' + 401 '</div>' + 402 '<div class="oc-cookie-prefs" id="oc-prefs">' + 403 '<div class="oc-prefs-card" role="dialog" aria-modal="true" aria-label="' + esc(T.prefsTitle) + '">' + 404 '<div class="oc-prefs-header">' + 405 '<div><h2>' + esc(T.prefsTitle) + '</h2><p>' + esc(T.prefsSubtitle) + '</p></div>' + 406 '<button class="oc-prefs-close" data-oc="close" title="' + esc(T.close) + '">×</button>' + 407 '</div>' + 408 '<div class="oc-prefs-body">' + categoriesHtml(stored) + '</div>' + 409 '<div class="oc-prefs-notice">' + esc(T.notice.replace('{DAYS}', CONFIG.expiryDays)) + '</div>' + 410 '<div class="oc-prefs-footer">' + 411 '<button class="oc-btn oc-btn-reject" data-oc="reject">' + esc(T.reject) + '</button>' + 412 '<button class="oc-btn oc-btn-save" data-oc="save">' + esc(T.save) + '</button>' + 413 '<button class="oc-btn oc-btn-accept" data-oc="accept">' + esc(T.accept) + '</button>' + 414 '</div>' + 415 '</div>' + 416 '</div>' + 417 '<button class="oc-cookie-reopen" id="oc-reopen" title="' + esc(T.reopenTitle) + '">ðª</button>'; 418 419 document.body.appendChild(root); 420 421 el.banner = document.getElementById('oc-banner'); 422 el.prefs = document.getElementById('oc-prefs'); 423 el.reopen = document.getElementById('oc-reopen'); 424 425 wireEvents(); 426 } 427 428 function categoriesHtml(stored) { 429 return CATEGORIES.map(function (c) { 430 var count = c.cookies.length + ' ' + (c.cookies.length === 1 ? T.cookieLabel : T.cookiesLabel); 431 var checked = c.locked ? true : (stored && stored.categories ? !!stored.categories[c.key] : c.def); 432 var input = '<input type="checkbox" id="oc-cat-' + c.key + '"' + 433 (checked ? ' checked' : '') + (c.locked ? ' disabled' : '') + '>'; 434 var rows = c.cookies.map(function (ck) { 435 return '<div class="oc-cookie-item"><span class="oc-name">' + esc(ck.name) + 436 '</span><span class="oc-provider">' + esc(ck.provider) + '</span></div>'; 437 }).join(''); 438 return '<div class="oc-cat" data-oc-cat>' + 439 '<div class="oc-cat-header" data-oc="expand">' + 440 '<span class="oc-arrow">▶</span>' + 441 '<div class="oc-cat-info"><h4>' + esc(c.name) + 442 ' <span class="oc-count">' + count + '</span></h4><p>' + esc(c.desc) + '</p></div>' + 443 '<label class="oc-toggle" data-oc-stop>' + input + '<span class="oc-slider"></span></label>' + 444 '</div>' + 445 '<div class="oc-cat-detail"><div class="oc-cookie-list">' + rows + '</div></div>' + 446 '</div>'; 447 }).join(''); 448 } 449 450 function wireEvents() { 451 el.banner.addEventListener('click', dispatch); 452 el.prefs.addEventListener('click', function (e) { 453 if (e.target === el.prefs) { closePrefs(); return; } 454 dispatch(e); 455 }); 456 el.reopen.addEventListener('click', function () { showBanner(); }); 457 document.addEventListener('keydown', function (e) { 458 if (e.key === 'Escape' && el.prefs.classList.contains('oc-visible')) { closePrefs(); } 459 }); 460 } 461 462 function dispatch(e) { 463 var stop = e.target.closest('[data-oc-stop]'); 464 var expand = e.target.closest('[data-oc="expand"]'); 465 var btn = e.target.closest('[data-oc]'); 466 if (stop) { e.stopPropagation(); return; } // toggle click â don't expand 467 if (expand) { 468 expand.parentNode.classList.toggle('oc-expanded'); 469 return; 470 } 471 if (!btn) { return; } 472 switch (btn.getAttribute('data-oc')) { 473 case 'accept': acceptAll(); break; 474 case 'reject': rejectAll(); break; 475 case 'save': savePrefs(); break; 476 case 'config': openPrefs(); break; 477 case 'close': closePrefs(); break; 478 } 479 } 480 481 function showBanner() { el.banner.classList.remove('oc-hidden'); el.reopen.classList.remove('oc-visible'); closePrefs(); } 482 function hideBanner() { el.banner.classList.add('oc-hidden'); el.reopen.classList.add('oc-visible'); } 483 function openPrefs() { syncToggles(); hideBanner(); el.prefs.classList.add('oc-visible'); el.reopen.classList.remove('oc-visible'); } 484 function closePrefs() { el.prefs.classList.remove('oc-visible'); } 485 486 // Sync the panel toggles with the currently stored consent (the panel DOM is 487 // built once, so a prior accept/reject must be reflected when reopening it). 488 function syncToggles() { 489 var c = readConsent(); 490 CATEGORIES.forEach(function (cat) { 491 var input = document.getElementById('oc-cat-' + cat.key); 492 if (!input) { return; } 493 if (cat.locked) { input.checked = true; return; } 494 input.checked = (c && c.categories) ? !!c.categories[cat.key] : cat.def; 495 }); 496 } 497 498 function esc(s) { 499 return String(s).replace(/[&<>"']/g, function (c) { 500 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 501 }); 502 } 503 504 /* ------------------------------------------------------------------ * 505 * 7. Init 506 * ------------------------------------------------------------------ */
507 function init() { 508 var stored = readConsent(); 509 510 // Re-apply a valid, non-expired prior consent on every page load. 511 if (stored && stored.categories) { 512 var ts = Date.parse(stored.timestamp); 513 var ageDays = isNaN(ts) ? Infinity : (Date.now() - ts) / 86400000; 514 if (ageDays <= (stored.expiry || CONFIG.expiryDays)) { 515 applyConsent(stored.categories, false); 516 build(stored); 517 el.reopen.classList.add('oc-visible'); // banner stays hidden, ðª available 518 return; 519 } 520 } 521 522 // No valid consent â show banner (default deny already set). 523 build(stored); 524 showBanner(); 525 } 526 527 if (document.readyState === 'loading') { 528 document.addEventListener('DOMContentLoaded', init); 529 } else { 530 init(); 531 } 532 533 // Public API (debug / programmatic reopen) 534 window.ocCookieConsent = { 535 reopen: function () { showBanner(); }, 536 get: readConsent, 537 acceptAll: acceptAll, 538 rejectAll: rejectAll 539 }; 540})();
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.