PageSourceSearch

https://kaltiot.ai/js/consent.js

js kaltiot.ai collected 2026-10-05 13:30:18 UTC 10,190 bytes, 254 lines download raw bytes

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.