1/* 2 * Cookie consent (banner + preferences panel). 3 * 4 * Stores a granular choice per category instead of a single "seen the banner" flag, 5 * and is the only thing allowed to load non-technical trackers: nothing that writes a 6 * non-essential cookie may be loaded until the matching category has been granted. 7 * 8 * Public API: window.OrmaCookieConsent 9 * .get() -> { technical, statistics, profiling, ts } | null (null = no choice yet) 10 * .allows(category) -> bool 11 * .onChange(fn) -> fn(state); fires immediately if a choice already exists 12 * .openPreferences() -> opens the second layer (used by the footer link) 13 */ 14(function (window, document) { 15 'use strict'; 16 17 var STORAGE_KEY = 'orma_cookie_consent'; 18 var LEGACY_KEY = 'Cookie'; 19 // Bump when the categories or the wording change: a bump re-asks everyone. 20 var VERSION = 1; 21 22 var listeners = []; 23 var current = null; 24 var root = null; 25 26 function readStored() { 27 try { 28 var raw = window.localStorage.getItem(STORAGE_KEY); 29 if (!raw) { 30 return null; 31 } 32 var parsed = JSON.parse(raw); 33 if (!parsed || parsed.v !== VERSION) { 34 return null; 35 } 36 return { 37 technical: true, 38 statistics: parsed.statistics === true, 39 profiling: parsed.profiling === true, 40 ts: parsed.ts 41 }; 42 } catch (e) { 43 return null; 44 } 45 } 46 47 function persist(state) { 48 try { 49 window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ 50 v: VERSION, 51 technical: true, 52 statistics: state.statistics === true, 53 profiling: state.profiling === true, 54 ts: state.ts 55 })); 56 // The old flag only recorded "banner dismissed" and carried no categories. 57 window.localStorage.removeItem(LEGACY_KEY); 58 } catch (e) { 59 /* private mode / storage disabled: the choice holds for this page only */ 60 } 61 } 62 63 function notify() { 64 for (var i = 0; i < listeners.length; i++) { 65 try { 66 listeners[i](current); 67 } catch (e) { 68 /* one bad listener must not block the others */ 69 } 70 } 71 } 72 73 function apply(statistics, profiling) { 74 current = { 75 technical: true, 76 statistics: statistics === true, 77 profiling: profiling === true, 78 ts: new Date().toISOString() 79 }; 80 persist(current); 81 hideBanner(); 82 closePreferences(); 83 notify(); 84 } 85 86 /* ----- Google consent mode / analytics loading ----- */ 87 88 var gtagLoaded = false; 89 90 function gtag() { 91 window.dataLayer.push(arguments); 92 } 93 94 function initGoogleConsent() { 95 window.dataLayer = window.dataLayer || []; 96 // Denied until the user says otherwise, and set before any Google tag can run. 97 gtag('consent', 'default', { 98 ad_storage: 'denied', 99 ad_user_data: 'denied', 100 ad_personalization: 'denied', 101 analytics_storage: 'denied', 102 functionality_storage: 'granted', 103 security_storage: 'granted' 104 }); 105 } 106 107 function syncGoogleConsent(state, measurementId) { 108 if (!state) { 109 return; 110 } 111 112 gtag('consent', 'update', { 113 ad_storage: state.profiling ? 'granted' : 'denied', 114 ad_user_data: state.profiling ? 'granted' : 'denied', 115 ad_personalization: state.profiling ? 'granted' : 'denied', 116 analytics_storage: state.statistics ? 'granted' : 'denied' 117 }); 118 119 if (!measurementId || gtagLoaded || !(state.statistics || state.profiling)) { 120 return; 121 } 122 123 gtagLoaded = true; 124 var script = document.createElement('script'); 125 script.async = true; 126 script.src = 'https://www.googletagmanager.com/gtag/js?i
126d=' + encodeURIComponent(measurementId); 127 document.head.appendChild(script); 128 129 gtag('js', new Date()); 130 gtag('config', measurementId); 131 } 132 133 /* ----- DOM ----- */ 134 135 function banner() { 136 return root ? root.querySelector('[data-cookie-banner]') : null; 137 } 138 139 function panel() { 140 return root ? root.querySelector('[data-cookie-prefs]') : null; 141 } 142 143 function showBanner() { 144 var el = banner(); 145 if (el) { 146 el.classList.remove('d-none'); 147 } 148 } 149 150 function hideBanner() { 151 var el = banner(); 152 if (el) { 153 el.classList.add('d-none'); 154 } 155 } 156 157 function toggleInput(category) { 158 return root ? root.querySelector('[data-cookie-toggle="' + category + '"]') : null; 159 } 160 161 function openPreferences() { 162 var el = panel(); 163 if (!el) { 164 return; 165 } 166 167 // Non-technical categories open unchecked unless the user previously granted them. 168 var stats = toggleInput('statistics'); 169 var prof = toggleInput('profiling'); 170 if (stats) { 171 stats.checked = current ? current.statistics === true : false; 172 } 173 if (prof) { 174 prof.checked = current ? current.profiling === true : false; 175 } 176 177 el.classList.remove('d-none'); 178 var focusable = el.querySelector('[data-cookie-action="save"]'); 179 if (focusable) { 180 focusable.focus(); 181 } 182 } 183 184 function closePreferences() { 185 var el = panel(); 186 if (el) { 187 el.classList.add('d-none'); 188 } 189 } 190 191 function onAction(action) { 192 if (action === 'accept-all') { 193 apply(true, true); 194 } else if (action === 'reject-all') { 195 apply(false, false); 196 } else if (action === 'open-preferences') { 197 openPreferences(); 198 } else if (action === 'save') { 199 var stats = toggleInput('statistics'); 200 var prof = toggleInput('profiling'); 201 apply(stats ? stats.checked : false, prof ? prof.checked : false); 202 } else if (action === 'close-preferences') { 203 closePreferences(); 204 } 205 } 206 207 function bind() { 208 root = document.querySelector('[data-cookie-consent]'); 209 if (!root) { 210 return; 211 } 212 213 document.addEventListener('click', function (e) { 214 var target = e.target && e.target.closest ? e.target.closest('[data-cookie-action]') : null; 215 if (!target) { 216 return; 217 } 218 e.preventDefault(); 219 onAction(target.getAttribute('data-cookie-action')); 220 }); 221 222 document.addEventListener('keydown', function (e) { 223 var el = panel(); 224 if (e.key === 'Escape' && el && !el.classList.contains('d-none')) { 225 closePreferences(); 226 } 227 }); 228 229 var overlay = panel(); 230 if (overlay) { 231 overlay.addEventListener('click', function (e) { 232 if (e.target === overlay) { 233 closePreferences(); 234 } 235 }); 236 } 237 238 var measurementId = root.getAttribute('data-ga-id') || ''; 239 api.onChange(function (state) { 240 syncGoogleConsent(state, measurementId); 241 }); 242 243 if (current) { 244 hideBanner(); 245 } else { 246 showBanner(); 247 } 248 } 249 250 var api = { 251 get: function () { 252 return current ? { 253 technical: true, 254 statistics: current.statistics, 255 profiling: current.profiling, 256 ts: current.ts 257 } : null; 258 }, 259 allows: function (category) { 260 if (category === 'technical') { 261 return true; 262 } 263 return !!current && current[category] === true; 264 }, 265 onChange: function (fn) { 266 if (typeof fn !== 'function') { 267 return; 268 } 269 listeners.push(fn); 270 if (current) { 271 fn(current); 272 } 273 }, 274 openPreferences: openPreferences, 275 acceptAll: function () { 276 apply(true, true); 277 }, 278 rejectAll: function () { 279 apply(false, false); 280 } 281 }; 282 283 initGoogleConsent(); 284 current = readStored(); 285 window.OrmaCookieConsent = api; 286 287 if (document.readyState === 'loading') { 288 document.addEventListener('DOMContentLoaded', bind); 289 } else { 290 bind(); 291 } 292})(window, document);
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.