https://www.bundesakademie-trossingen.de/_assets/b9a689cb028cb6925…/JavaScript/consent.js?1787736054
1/* ============================================================================ 2 w52_consent â GDPR consent banner. 3 4 Reads its configuration (storage key, declaration version, expiry, 5 groups with their opt-in script URLs and cleanup rules) from the banner's 6 data-consent-config attribute; the values originate from the project's 7 YAML declaration (ConsentConfigurationProcessor). 8 9 The choice is stored in localStorage as 10 { "version": <int>, "date": "<ISO>", "groups": { "<key>": true|false } } 11 and expires after the configured number of months or whenever the 12 declaration version changes (re-consent). 13 14 Lifecycle per group 15 grant Scripts of required groups always run; scripts of optional 16 groups load only after explicit opt-in â strictly in the 17 declared order (the next script is inserted once the previous 18 one has executed). A script that fails to load is retried the 19 next time the choice is applied; the rest of its group is 20 skipped for that run because a dependency is missing. 21 withdraw When a previously granted group is switched off, its declared 22 cookies and storage entries are deleted and the 23 "w52consent:changed" event lists the group in 24 detail.withdrawn so site scripts can remove their embeds. 25 A running script cannot be unloaded, therefore groups with 26 scripts reload the page after the withdrawal has been stored 27 (YAML: reloadOnWithdraw, defaults to true for groups with 28 scripts). Cookies/storage of groups without consent are also 29 swept on every page view in case a service re-created them. 30 31 The banner is a native <dialog> opened with showModal(): the page behind 32 it is inert, focus starts on the heading and returns to the element that 33 opened the banner (e.g. the footer button) when it closes. Esc closes 34 the dialog without storing anything â nothing optional runs until a 35 choice has been made, the banner reappears on the next page view. 36 37 Everything is file-based: no inline scripts, safe with a strict CSP. 38 Without JavaScript nothing is shown and nothing loads. 39 40 Public API for site scripts (services that are not plain scripts, 41 e.g. iframes/embeds gated behind a consent group): 42 window.w52Consent.has('<groupKey>') -> boolean (required groups: true) 43 window.w52Consent.open() -> reopen the banner 44 plus a document event fired on every stored/applied decision: 45 document.addEventListener('w52consent:changed', function (e) { 46 e.detail.groups // { '<groupKey>': true|false } 47 e.detail.withdrawn // [ '<groupKey>', ... ] switched off just now 48 }); 49 Query has() at startup AND listen to the event â script order between 50 this file and site scripts is then irrelevant. 51 ========================================================================= */ 52(function () { 53 "use strict"; 54 55 var banner = document.getElementById("w52-consent-banner"); 56 if (!banner) return; 57 58 var config; 59 try { 60 config = JSON.parse(banner.getAttribute("data-consent-config")); 61 } catch (e) { 62 return; 63 } 64 var expiryMs = (config.expiryMonths || 12) * 30.44 * 24 * 60 * 60 * 1000; 65 var title = document.getElementById("w52-consent-title"); 66 var scriptLoads = {}; /* src -> Promise<boolean>; removed again on failure */ 67 var groupQueues = {}; /* group key -> Promise; serializes loads per group */ 68 var granted = {}; /* group key -> true while its services run in this page view */ 69 var opener = null; /* element that opened the banner; gets focus back */ 70 71 /* ---- stored choice --------------------------------------------------- */ 72 73 function readChoice() { 74 try { 75 var raw = window.localStorage.getItem(config.storageKey); 76 if (!raw) return null; 77 var choice = JSON.parse(raw); 78 if (!choice || choice.version !== config.version || typeof choice.groups !== "object") return null; 79 var age = Date.now() - Date.parse(choice.date); 80 if (!(age >= 0) || age > expiryMs) return null; /* expired or invalid */ 81 return choice; 82 } catch (e) { 83 return null; 84 } 85 } 86 87 function saveChoice(groups) { 88 try { 89 window.localStorage.setItem(config.storageKey, JSON.stringify({ 90 version: config.version, 91 date: new Date().toISOString(), 92 groups: groups 93 }));
94 (config.legacyStorageKeys || []).forEach(function (key) { 95 window.localStorage.removeItem(key); 96 }); 97 } catch (e) { /* storage blocked: the banner reappears on the next visit */ } 98 } 99 100 /* ---- script loading -------------------------------------------------- */ 101 102 function loadScript(src) { 103 if (!scriptLoads[src]) { 104 scriptLoads[src] = new Promise(function (resolve) { 105 var script = document.createElement("script"); 106 script.src = src; 107 script.addEventListener("load", function () { resolve(true); }); 108 script.addEventListener("error", function () { 109 delete scriptLoads[src]; /* allow a retry on the next applyChoice() */ 110 script.remove(); 111 resolve(false); 112 }); 113 document.head.appendChild(script); 114 }); 115 } 116 return scriptLoads[src]; 117 } 118 119 /* Scripts of one group run strictly in declaration order: the next one is 120 only inserted after the previous one has executed (dynamically inserted 121 scripts do not keep their order otherwise). Runs for the same group are 122 queued behind each other. */ 123 function loadScripts(group) { 124 var scripts = group.scripts || []; 125 groupQueues[group.key] = (groupQueues[group.key] || Promise.resolve()).then(function () { 126 return scripts.reduce(function (chain, src) { 127 return chain.then(function (ok) { return ok ? loadScript(src) : false; }); 128 }, Promise.resolve(true)); 129 }); 130 } 131 132 /* ---- cleanup of withdrawn / non-consented groups ---------------------- */ 133 134 /* "name" matches exactly, "prefix*" matches every name starting with prefix */ 135 function matches(name, pattern) { 136 return pattern.slice(-1) === "*" 137 ? name.indexOf(pattern.slice(0, -1)) === 0 138 : name === pattern; 139 } 140 141 function cookieNames() { 142 return document.cookie.split(";").map(function (cookie) { 143 return cookie.split("=")[0].trim(); 144 }).filter(Boolean); 145 } 146 147 /* A cookie can only be deleted with the domain/path it was set for, which 148 we do not know â so expire it for the host-only variant, every domain 149 level of the current host, and the root as well as the current path. */ 150 function expireCookie(name) { 151 var expires = "; expires=Thu, 01 Jan 1970 00:00:00 GMT"; 152 var secure = window.location.protocol === "https:" ? "; secure" : ""; 153 var paths = ["/"]; 154 if (window.location.pathname !== "/") paths.push(window.location.pathname); 155 var parts = window.location.hostname.split("."); 156 var domains = [""]; 157 for (var i = 0; i < parts.length - 1; i++) { 158 domains.push("; domain=" + parts.slice(i).join(".")); 159 } 160 paths.forEach(function (path) { 161 domains.forEach(function (domain) { 162 document.cookie = name + "=" + expires + "; path=" + path + domain + secure; 163 }); 164 }); 165 } 166 167 function removeStorageEntries(storage, pattern) { 168 var keys = []; 169 for (var i = 0; i < storage.length; i++) keys.push(storage.key(i));
170 keys.forEach(function (key) { 171 if (key !== config.storageKey && matches(key, pattern)) storage.removeItem(key); 172 }); 173 } 174 175 /* Deletes what a group declared: its cookies (exact name or "prefix*") 176 and its localStorage/sessionStorage entries. Cookies a third party set 177 on its own domain are out of reach â such services rely on 178 reloadOnWithdraw (or the site script reacting to the event). */ 179 function cleanup(group) { 180 try { 181 var names = cookieNames(); 182 (group.cookies || []).forEach(function (pattern) { 183 names.forEach(function (name) { 184 if (matches(name, pattern)) expireCookie(name); 185 }); 186 }); 187 (group.storageKeys || []).forEach(function (pattern) { 188 removeStorageEntries(window.localStorage, pattern); 189 removeStorageEntries(window.sessionStorage, pattern); 190 }); 191 } catch (e) { /* cookie/storage access blocked */ } 192 } 193 194 /* ---- applying a choice ----------------------------------------------- */ 195 196 /* Starts newly granted groups, cleans up everything without consent and 197 returns the groups that were running but are now withdrawn. */ 198 function applyChoice(groups) { 199 var withdrawn = []; 200 config.groups.forEach(function (group) { 201 var allowed = group.required || !!(groups && groups[group.key]); 202 if (allowed && !granted[group.key]) { 203 granted[group.key] = true; 204 loadScripts(group); 205 } else if (!allowed && granted[group.key]) { 206 delete granted[group.key]; 207 withdrawn.push(group); 208 } 209 if (!allowed) cleanup(group); 210 }); 211 return withdrawn; 212 } 213 214 function emitChange(groups, withdrawn) { 215 document.dispatchEvent(new CustomEvent("w52consent:changed", { 216 detail: { groups: groups, withdrawn: withdrawn } 217 })); 218 } 219 220 function decide(groups) { 221 saveChoice(groups); 222 closeBanner(); 223 var withdrawn = applyChoice(groups); 224 emitChange(groups, withdrawn.map(function (group) { return group.key; })); 225 if (withdrawn.some(function (group) { return group.reloadOnWithdraw; })) { 226 /* A running script cannot be unloaded â start over without it. The 227 choice is already stored, so the banner stays closed. */ 228 window.location.reload(); 229 } 230 } 231 232 function collectToggles(value) { 233 var groups = {}; 234 config.groups.forEach(function (group) { 235 if (group.required) return; 236 if (typeof value === "boolean") { 237 groups[group.key] = value; 238 } else { 239 var toggle = banner.querySelector('[data-consent-toggle="' + group.key + '"]'); 240 groups[group.key] = !!(toggle && toggle.checked); 241 } 242 }); 243 return groups; 244 } 245 246 /* ---- dialog ---------------------------------------------------------- */ 247 248 function openBanner() { 249 if (banner.hasAttribute("open")) return; 250 opener = document.activeElement; 251 var choice = readChoice(); 252 config.groups.forEach(function (group) { 253 if (group.required) return; 254 var toggle = banner.querySelector('[data-consent-toggle="' + group.key + '"]'); 255 if (toggle) toggle.checked = !!(choice && choice.groups[group.key]); 256 }); 257 if (typeof banner.showModal === "function") { 258 banner.showModal(); /* top layer; the page behind becomes inert */ 259 } else { 260 banner.setAttribute("open", ""); 261 } 262 /* Long content: start on the heading rather than the first button so 263 screen reader users hear title and intro before the choices. */ 264 (title || banner).focus(); 265 } 266 267 function restoreFocus() { 268 if (opener && opener !== document.body && typeof opener.focus === "function" && document.body.contains(opener)) { 269 opener.focus(); 270 } 271 opener = null; 272 } 273 274 function closeBanner() { 275 if (!banner.hasAttribute("open")) return; 276 if (typeof banner.close === "function") { 277 banner.close(); 278 } else { 279 banner.removeAttribute("open"); 280 } 281 restoreFocus(); 282 } 283 284 /* Esc closes a modal dialog natively (cancel -> close) without a decision: 285 nothing is stored, only the focus is handed back. */ 286 banner.addEventListener("close", restoreFocus); 287 288 banner.addEventListener("click", function (e) { 289 if (e.target.closest("[data-consent-accept-all]")) { 290 decide(collectToggles(true)); 291 } else if (e.target.closest("[data-consent-required-only]")) { 292 decide(collectToggles(false)); 293 } else if (e.target.closest("[data-consent-save]")) { 294 decide(collectToggles()); 295 } 296 }); 297 298 /* Reopen buttons (e.g. footer) are only useful with JavaScript: reveal 299 them here and wire them up. */ 300 var openers = document.querySelectorAll("[data-consent-open]"); 301 for (var i = 0; i < openers.length; i++) { 302 openers[i].removeAttribute("hidden"); 303 openers[i].addEventListener("click", function () { openBanner(); }); 304 } 305 306 window.w52Consent = { 307 has: function (groupKey) { 308 for (var i = 0; i < config.groups.length; i++) { 309 if (config.groups[i].key !== groupKey) continue; 310 if (config.groups[i].required) return true; 311 var choice = readChoice(); 312 return !!(choice && choice.groups[groupKey]); 313 } 314 return false;
315 }, 316 open: function () { openBanner(); } 317 }; 318 319 var choice = readChoice(); 320 applyChoice(choice ? choice.groups : null); /* required groups run regardless */ 321 if (choice) { 322 emitChange(choice.groups, []); 323 } else { 324 openBanner(); 325 } 326})();
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.