1/** 2 * Kaltiot cookie consent + Google Consent Mode v2 handler. 3 * 4 * Expects window.dataLayer and gtag() to already exist (set by the inline 5 * consent-defaults snippet in <head>, before GTM loads). 6 * 7 * Storage: localStorage key "kaltiot_consent" with 8 * { analytics, marketing, preferences, _ts } (granted/denied strings, ms epoch) 9 * Re-prompts user after 180 days. 10 * 11 * Public API: 12 * window.KaltiotConsent.show() // re-open banner 13 * window.KaltiotConsent.get() // current state 14 * window.KaltiotConsent.accept('all'|'none'|obj) // programmatic choice 15 */ 16(function () { 17 const STORAGE_KEY = 'kaltiot_consent'; 18 const EXPIRY_DAYS = 180; 19 const EXPIRY_MS = EXPIRY_DAYS * 24 * 60 * 60 * 1000; 20 21 const htmlLang = (document.documentElement.getAttribute('lang') || 'fi').toLowerCase(); 22 const lang = htmlLang.startsWith('en') ? 'en' : 'fi'; 23 24 const T = { 25 fi: { 26 title: 'Kaltiot käyttää evästeitä', 27 intro: 'Käytämme evästeitä sivuston toiminnan varmistamiseen, kävijäanalytiikkaan ja markkinoinnin kohdentamiseen. Voit hyväksyä kaikki, hylätä ei-välttämättömät tai mukauttaa asetuksia.', 28 policyLinks: 'Lue lisää: <a href="/cookie-policy/">Evästekäytäntö</a> · <a href="/privacy-policy/">Tietosuojaseloste</a>', 29 acceptAll: 'Hyväksy kaikki', 30 rejectAll: 'Hylkää ei-välttämättömät', 31 customize: 'Asetukset', 32 save: 'Tallenna valinnat', 33 back: 'Takaisin', 34 necessary: { name: 'Välttämättömät', desc: 'Tarvitaan sivuston perustoiminnan kannalta. Näitä ei voi poistaa käytöstä.' }, 35 analytics: { name: 'Analytiikka', desc: 'Anonymisoitu kävijäseuranta (Google Analytics 4) sivuston kehittämiseksi.' }, 36 marketing: { name: 'Markkinointi', desc: 'Mainospikselit ja kohdennus (esim. LinkedIn Insight Tag). Käytetään vain kun markkinointi on käytössä.' }, 37 preferences: { name: 'Mieltymykset', desc: 'Muistaa esim. kieli- ja UI-valintasi paremman käyttökokemuksen vuoksi.' }, 38 change: 'Evästeasetukset', 39 }, 40 en: { 41 title: 'Kaltiot uses cookies', 42 intro: 'We use cookies for essential site functions, visitor analytics, and marketing measurement. Accept all, reject non-essential, or customize your choices.', 43 policyLinks: 'Learn more: <a href="/cookie-policy/">Cookie Policy</a> · <a href="/privacy-policy/">Privacy Policy</a>', 44 acceptAll: 'Accept all', 45 rejectAll: 'Reject non-essential', 46 customize: 'Customize', 47 save: 'Save choices', 48 back: 'Back', 49 necessary: { name: 'Necessary', desc: 'Required for basic site functionality. Cannot be disabled.' }, 50 analytics: { name: 'Analytics', desc: 'Anonymized visitor analytics (Google Analytics 4) used to improve the site.' }, 51 marketing: { name: 'Marketing', desc: 'Advertising pixels and targeting (e.g. LinkedIn Insight Tag). Used only when marketing is enabled.' }, 52 preferences: { name: 'Preferences', desc: 'Remembers your language and UI choices for a better experience.' }, 53 change: 'Cookie settings', 54 }, 55 }; 56 const t = T[lang]; 57 58 function gtag() { 59 window.dataLayer = window.dataLayer || []; 60 window.dataLayer.push(arguments); 61 } 62 63 function loadStored() { 64 try { 65 const raw = localStorage.getItem(STORAGE_KEY); 66 if (!raw) return null; 67 const obj = JSON.parse(raw); 68 if (!obj || !obj._ts) return null; 69 if (Date.now() - obj._ts > EXPIRY_MS) return null; 70 return obj; 71 } catch (e) { 72 return null; 73 } 74 } 75 76 function save(choices) { 77 const payload = { ...choices, _ts: Date.now() }; 78 try { localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch (e) {} 79 applyConsent(choices); 80 gtag('event', 'consent_update', { 81 consent_analytics: choices.analytics, 82 consent_marketing: choices.marketing, 83 }); 84 } 85 86 function applyConsent(choices) { 87 gtag('consent', 'update', { 88 analytics_storage: choices.analytics === 'granted' ? 'granted' : 'denied', 89 ad_storage: choices.marketing === 'granted' ? 'granted' : 'denied', 90 ad_user_data: choices.marketing === 'granted' ? 'granted' : 'denied', 91 ad_personalization: choices.marketing === 'granted' ? 'granted' : 'denied', 92 functionality_storage: choices.preferences === 'granted' ? 'granted' : 'denied', 93 personalization_storage: choices.preferences === 'granted' ? 'granted' : 'denied', 94 security_storage: 'granted', 95 }); 96 } 97 98 function buildBanner() { 99 const wrap = document.createElement('div'); 100 wrap.id = 'kaltiot-consent'; 101 wrap.className = 'kconsent'; 102 wrap.setAttribute('role', 'dialog'); 103 wrap.setAttribute('aria-live', 'polite'); 104 wrap.setAttribute('aria-label', t.title); 105 106 wrap.innerHTML = ` 107 <div class="kconsent-panel" data-view="main"> 108 <div class="kconsent-main"> 109 <h3 class="kconsent-title">${t.title}</h3> 110 <p class="kconsent-intro">${t.intro}</p> 111 <p class="kconsent-links">${t.policyLinks}</p> 112 <div class="kconsent-actions"> 113 <button type="button" class="kconsent-btn kconsent-btn--ghost" data-action="reject">${t.rejectAll}</button> 114 <button type="button" class="kconsent-btn kconsent-btn--ghost" data-action="customize">${t.customize}</button> 115 <button type="button" class="kconsent-btn kconsent-btn--primary" data-action="accept">${t.acceptAll}</button> 116 </div> 117 </div> 118 <div class="kconsent-details" hidden> 119 <h3 class="kconsent-title">${t.customize}</h3> 120 <ul class="kconsent-categories"> 121 ${categoryRow('necessary', true, true)} 122 ${categoryRow('analytics', false, false)} 123 ${categoryRow('marketing', false, false)} 124 ${categoryRow('preferences', false, false)} 125 </ul> 126 <div class="kconsent-actions"> 127 <button type="button" class="kconsent-btn kconsent-btn--ghost" data-action="back">${t.back}</button> 128 <button type="button" class="kconsent-btn kconsent-btn--primary" data-action="save">
128${t.save}</button> 129 </div> 130 </div> 131 </div>`; 132 return wrap; 133 } 134 135 function categoryRow(key, checked, disabled) { 136 const c = t[key]; 137 return ` 138 <li class="kconsent-category"> 139 <label class="kconsent-row"> 140 <input type="checkbox" data-cat="${key}" ${checked ? 'checked' : ''} ${disabled ? 'disabled' : ''} /> 141 <span class="kconsent-row-text"> 142 <span class="kconsent-row-name">${c.name}</span> 143 <span class="kconsent-row-desc">${c.desc}</span> 144 </span> 145 </label> 146 </li>`; 147 } 148 149 let bannerEl = null; 150 151 function ensureBanner() { 152 if (bannerEl) return bannerEl; 153 bannerEl = buildBanner(); 154 document.body.appendChild(bannerEl); 155 bindEvents(bannerEl); 156 return bannerEl; 157 } 158 159 function showBanner(prefill) { 160 const el = ensureBanner(); 161 el.classList.add('kconsent--visible'); 162 showView(el, 'main'); 163 if (prefill) { 164 el.querySelectorAll('input[data-cat]').forEach(cb => { 165 const k = cb.dataset.cat; 166 if (k === 'necessary') return; 167 cb.checked = prefill[k] === 'granted'; 168 }); 169 } 170 } 171 172 function hideBanner() { 173 if (bannerEl) bannerEl.classList.remove('kconsent--visible'); 174 } 175 176 function showView(el, view) { 177 el.querySelector('.kconsent-main').hidden = view !== 'main'; 178 el.querySelector('.kconsent-details').hidden = view !== 'details'; 179 el.querySelector('.kconsent-panel').setAttribute('data-view', view); 180 } 181 182 function bindEvents(el) { 183 el.addEventListener('click', (e) => { 184 const btn = e.target.closest('[data-action]'); 185 if (!btn) return; 186 const action = btn.dataset.action; 187 if (action === 'accept') { 188 save({ analytics: 'granted', marketing: 'granted', preferences: 'granted' }); 189 hideBanner(); 190 } else if (action === 'reject') { 191 save({ analytics: 'denied', marketing: 'denied', preferences: 'denied' }); 192 hideBanner(); 193 } else if (action === 'customize') { 194 showView(el, 'details'); 195 } else if (action === 'back') { 196 showView(el, 'main'); 197 } else if (action === 'save') { 198 const choices = { analytics: 'denied', marketing: 'denied', preferences: 'denied' }; 199 el.querySelectorAll('input[data-cat]').forEach(cb => { 200 const k = cb.dataset.cat; 201 if (k === 'necessary') return; 202 choices[k] = cb.checked ? 'granted' : 'denied'; 203 }); 204 save(choices); 205 hideBanner(); 206 } 207 }); 208 } 209 210 function addFooterLink() { 211 document.querySelectorAll('.footer-links ul').forEach(ul => { 212 if (ul.querySelector('[data-kconsent-link]')) return; 213 const cookiePolicyItem = ul.querySelector('a[href*="/cookie-policy/"]'); 214 if (!cookiePolicyItem) return; 215 const li = document.createElement('li'); 216 const a = document.createElement('a'); 217 a.href = '#'; 218 a.setAttribute('data-kconsent-link', ''); 219 a.textContent = t.change; 220 a.addEventListener('click', (e) => { e.preventDefault(); showBanner(window.KaltiotConsent.get()); }); 221 li.appendChild(a); 222 cookiePolicyItem.closest('li').after(li); 223 }); 224 } 225 226 function init() { 227 const stored = loadStored(); 228 if (stored) { 229 applyConsent(stored); 230 } else { 231 showBanner(); 232 } 233 // Footer may be injected by template.js after this runs; observe briefly 234 addFooterLink(); 235 setTimeout(addFooterLink, 200); 236 setTimeout(addFooterLink, 800); 237 } 238 239 window.KaltiotConsent = { 240 show: () => showBanner(loadStored() || undefined), 241 get: () => loadStored(), 242 accept: (what) => { 243 if (what === 'all') save({ analytics: 'granted', marketing: 'granted', preferences: 'granted' }); 244 else if (what === 'none') save({ analytics: 'denied', marketing: 'denied', preferences: 'denied' }); 245 else if (what && typeof what === 'object') save(what); 246 }, 247 }; 248 249 if (document.readyState === 'loading') { 250 document.addEventListener('DOMContentLoaded', init); 251 } else { 252 init(); 253 } 254})();
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.