PageSourceSearch

https://educhess.ru/wp-content/themes/educhess-theme/assets/js/cookie-consent.js?ver=0.10.70

js educhess.ru collected 2026-10-04 22:20:48 UTC 14,113 bytes, 368 lines download raw bytes

1(() => {
2  'use strict';
3
4  const config = window.EduChessConfig || {};
5  const storageKey = 'educhess_cookie_consent';
6  const consentVersion = String(config.cookieVersion || '2026-08-16-v2');
7  const policyVersion = String(config.privacyPolicyVersion || '2026-08-16');
8  const lifetimeDays = Math.max(1, Number(config.cookieLifetimeDays || 180));
9  const banner = document.querySelector('[data-cookie-banner]');
10  const modal = document.querySelector('[data-cookie-modal]');
11  const dialog = modal?.querySelector('.cookie-modal__dialog');
12  const analyticsInput = modal?.querySelector('[data-cookie-analytics]');
13  const marketingInput = modal?.querySelector('[data-cookie-marketing]');
14  let lastFocus = null;
15  let metrikaLoaded = false;
16  let calltouchLoaded = false;
17  let cachedClientId = '';
18
19  const metrikaId = () => Number(config.metrikaCounterId || 0);
20  const disableKey = () => `disableYaCounter${metrikaId()}`;
21  const calltouchModId = () => String(config.calltouchModId || '').trim();
22  const canLoadCalltouch = () => Boolean(config.calltouchEnabled && calltouchModId());
23
24  const phoneDigits = (value) => {
25    let digits = String(value || '').replace(/\D+/g, '');
26    if (digits.length === 11 && digits.startsWith('8')) digits = `7${digits.slice(1)}`;
27    return digits;
28  };
29
30  const telHref = (value) => {
31    const digits = phoneDigits(value);
32    return digits ? `+${digits}` : '';
33  };
34
35  const setDisplayedPhone = (targetValue) => {
36    const primary = String(config.primaryPhone || '').trim();
37    const target = String(targetValue || '').trim();
38    const primaryDigits = phoneDigits(primary);
39    const targetTel = telHref(target);
40    if (!primaryDigits || !targetTel) return;
41
42    document.querySelectorAll('a[href^="tel:"]').forEach((link) => {
43      const currentDigits = phoneDigits(link.getAttribute('href') || '');
44      const storedPrimary = phoneDigits(link.dataset.educhessPrimaryPhone || '');
45      if (currentDigits !== primaryDigits && storedPrimary !== primaryDigits) return;
46      if (!link.dataset.educhessPrimaryPhone) link.dataset.educhessPrimaryPhone = primary;
47      link.setAttribute('href', `tel:${targetTel}`);
48
49      const walker = document.createTreeWalker(link, NodeFilter.SHOW_TEXT);
50      const nodes = [];
51      while (walker.nextNode()) nodes.push(walker.currentNode);
52      nodes.forEach((node) => {
53        const text = String(node.nodeValue || '');
54        if (phoneDigits(text) === primaryDigits || phoneDigits(text) === currentDigits) {
55          const leading = text.match(/^\s*/)?.[0] || '';
56          const trailing = text.match(/\s*$/)?.[0] || '';
57          node.nodeValue = `${leading}${target}${trailing}`;
58        }
59      });
60    });
61  };
62
63  const restorePrimaryPhone = () => {
64    const primary = String(config.primaryPhone || '').trim();
65    if (primary) setDisplayedPhone(primary);
66  };
67
68  const applyPhoneMode = (analyticsAllowed) => {
69    if (analyticsAllowed) {
70      restorePrimaryPhone();
71      return;
72    }
73    const fallback = String(config.calltouchStaticPhone || '').trim();
74    if (config.calltouchEnabled && fallback) setDisplayedPhone(fallback);
75    else restorePrimaryPhone();
76  };
77
78  const parse = (raw) => {
79    if (!raw) return null;
80    try {
81      const value = JSON.parse(raw);
82      if (!value || value.necessary !== true) return null;
83      return value;
84    } catch (_) {
85      return null;
86    }
87  };
88
89  const readCookie = () => {
90    const row = document.cookie.split('; ').find((part) => part.startsWith(`${storageKey}=`));
91    return row ? decodeURIComponent(row.split('=').slice(1).join('=')) : null;
92  };
93
94  const read = () => {
95    const fromCookie = parse(readCookie());
96    if (fromCookie) return fromCookie;
97    try {
98      return parse(window.localStorage.getItem(storageKey));
99    } catch (_) {
100      return null;
101    }
102  };
103
104  const current = (value) => Boolean(
105    value &&
106    value.consentVersion === consentVersion &&
107    value.policyVersion === policyVersion
108  );
109
110  const persist = (value) => {
111    const raw = JSON.stringify(value);
112    const maxAge = Math.round(lifetimeDays * 86400);
113    const secure = window.location.protocol === 'https:' ? '; Secure' : '';
114    document.cookie = `${storageKey}=${encodeURIComponent(raw)}; Max-Age=${maxAge}; Path=/; SameSite=Lax${secure}`;
115    try {
116      window.localStorage.setItem(storageKey, raw);
117    } catch (_) {
118      // The first-party cookie remains the primary record if localStorage fails.
119    }
120  };
121
122  const setMetrikaDisabled = (disabled) => {
123    const id = metrikaId();
124    if (!id) return;
125    window[disableKey()] = Boolean(disabled);
126  };
127
128  const loadMetrika = () => {
129    const id = metrikaId();
130    if (!id || metrikaLoaded) return;
131    setMetrikaDisabled(false);
132    metrikaLoaded = true;
133    window.ym = window.ym || function () { (window.ym.a = window.ym.a || []).push(arguments); };
134    window.ym.l = Date.now();
135    const script = document.createElement('script');
136    script.async = true;
137    script.src = 'https://mc.yandex.ru/metrika/tag.js';
138    script.dataset.educhessMetrika = String(id);
139    document.head.appendChild(script);
140    window.ym(id, 'init', {
141      clickmap: true,
142      trackLinks: true,
143      accurateTrackBounce: true,
144      webvisor: true,
145    });
146  };
147
148  const loadCalltouch = () => {
149    if (!canLoadCalltouch() || calltouchLoaded || !window.EduChessConsent?.analytics) return;
150
151    // Calltouch starts collecting visit/session data as soon as its dynamic
152    // script is initialized, therefore the official loader is executed only
153    // after explicit analytics consent. The dynamic number must start from the
154    // normal EduChess phone, not from the optional static no-consent fallback.
155    restorePrimaryPhone();
156    calltouchLoaded = true;
157
158    const modId = calltouchModId();
159    window.CalltouchDataObject = 'ct';
160    window.ct = function () { window.ct.callbacks.push(arguments); };
161    window.ct.callbacks = [];
162    window.ct.loaded = false;
163    window.ct.counters = [modId];
164
165    const firstScript = document.getElementsByTagName('script')[0];
166    const script = document.createElement('script');
167    script.async = true;
168    script.src = `https://mod.calltouch.ru/${typeof Array.prototype.find === 'function' ? 'init-min.js' : 'init.js'}?id=${encodeURIComponent(modId)}`;
169    script.dataset.educhessCalltouch = modId;
170
171    if (firstScript?.parentNode) firstScript.parentNode.insertBefore(script, firstScript);
172    else document.head.appendChild(script);
173  };
174
175  const expireCookie = (name) => {
176    const secure = window.location.protocol === 'https:' ? '; Secure' : '';
177    const host = window.location.hostname;
178    const domains = ['', host, `.${host}`];
179    domains.forEach((domain) => {
180      const domainPart = domain ? `; Domain=${domain}` : '';
181      document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax${secure}${domainPart}`;
182    });
183  };
184
185  const clearOptionalAnalyticsCookies = () => {
186    [
187      '_ym_uid', '_ym_d', '_ym_isad', '_ym_visorc', '_ym_debug',
188      '_ct', '_ct_server', '_ct_client_global_id', '_ct_session_id',
189      '_ct_site_id', '_ct_ids', 'cted', 'call_s',
190    ].forEach(expireCookie);
191    cachedClientId = '';
192  };
193
194  const apply = (preferences) => {
195    window.EduChessConsent = preferences;
196    setMetrikaDisabled(!preferences.analytics);
197    applyPhoneMode(Boolean(preferences.analytics));
198    if (preferences.analytics) {
199      loadMetrika();
200      // Metrika is initialized first so Calltouch can read the available
201      // analytics context when its script starts.
202      window.setTimeout(loadCalltouch, 350);
203    }
204    document.dispatchEvent(new CustomEvent('educhess:consent', { detail: preferences }));
205  };
206
207  const store = ({ analytics, marketing }) => {
208    const value = {
209      necessary: true,
210      analytics: Boolean(analytics),
211      marketing: Boolean(marketing),
212      consentVersion,
213      policyVersion,
214      updatedAt: new Date().toISOString(),
215    };
216    persist(value);
217    apply(value);
218    return value;
219  };
220
221  const focusable = () => dialog
222    ? Array.from(dialog.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'))
223      .filter((element) => !element.hasAttribute('hidden'))
224    : [];
225
226  const openModal = () => {
227    if (!modal) return;
228    const saved = read() || { analytics: false, marketing: false };
229    if (analyticsInput) analyticsInput.checked = Boolean(saved.analytics);
230    if (marketingInput) marketingInput.checked = Boolean(saved.marketing);
231    lastFocus = document.activeElement;
232    modal.hidden = false;
233    modal.setAttribute('aria-hidden', 'false');
234    document.body.classList.add('cookie-modal-open');
235    requestAnimationFrame(() => dialog?.focus());
236  };
237
238  const closeModal = () => {
239    if (!modal || modal.hidden) return;
240    modal.hidden = true;
241    modal.setAttribute('aria-hidden', 'true');
242    document.body.classList.remove('cookie-modal-open');
243    if (lastFocus instanceof HTMLElement) lastFocus.focus();
244  };
245
246  const finish = (preferences) => {
247    const before = current(read()) ? Boolean(read()?.analytics) : false;
248    const after = Boolean(preferences.analytics);
249    store(preferences);
250    if (banner) banner.hidden = true;
251    closeModal();
252
253    // Once a third-party analytics script has run, disabling its global flag is
254    // not enough to unload it. Reload after an explicit withdrawal so the next
255    // document starts clean and no optional script is requested.
256    if (before && !after) {
257      clearOptionalAnalyticsCookies();
258      setMetrikaDisabled(true);
259      window.location.reload();
260    }
261  };
262
263  document.querySelectorAll('[data-cookie-open]').forEach((button) =>
263 button.addEventListener('click', openModal));
264  document.querySelectorAll('[data-cookie-close]').forEach((button) => button.addEventListener('click', closeModal));
265  document.querySelectorAll('[data-cookie-accept]').forEach((button) => button.addEventListener('click', () => finish({ analytics: true, marketing: true })));
266  document.querySelectorAll('[data-cookie-essential]').forEach((button) => button.addEventListener('click', () => finish({ analytics: false, marketing: false })));
267  document.querySelector('[data-cookie-save]')?.addEventListener('click', () => finish({
268    analytics: Boolean(analyticsInput?.checked),
269    marketing: Boolean(marketingInput?.checked),
270  }));
271
272  document.addEventListener('keydown', (event) => {
273    if (!modal || modal.hidden) return;
274    if (event.key === 'Escape') {
275      event.preventDefault();
276      closeModal();
277      return;
278    }
279    if (event.key !== 'Tab') return;
280    const items = focusable();
281    if (!items.length) return;
282    const first = items[0];
283    const last = items[items.length - 1];
284    if (event.shiftKey && document.activeElement === first) {
285      event.preventDefault();
286      last.focus();
287    } else if (!event.shiftKey && document.activeElement === last) {
288      event.preventDefault();
289      first.focus();
290    }
291  });
292
293  const saved = read();
294  if (current(saved)) {
295    apply(saved);
296  } else {
297    setMetrikaDisabled(true);
298    applyPhoneMode(false);
299    try { window.localStorage.removeItem(storageKey); } catch (_) {}
300    document.cookie = `${storageKey}=; Max-Age=0; Path=/; SameSite=Lax`;
301    if (banner) banner.hidden = false;
302  }
303
304  window.EduChessAnalytics = {
305    reachGoal(goal, params = {}) {
306      const id = metrikaId();
307      if (!id || !window.EduChessConsent?.analytics || typeof window.ym !== 'function' || !goal) return;
308      window.ym(id, 'reachGoal', String(goal), params);
309    },
310    trackEvent(eventName, params = {}) {
311      const id = metrikaId();
312      if (!id || !window.EduChessConsent?.analytics || typeof window.ym !== 'function' || !eventName) return;
313      window.ym(id, 'params', {
314        educhess_event: {
315          name: String(eventName),
316          ...params,
317        },
318      });
319    },
320    getClientID() {
321      const id = metrikaId();
322      if (cachedClientId) return Promise.resolve(cachedClientId);
323      if (!id || !window.EduChessConsent?.analytics || typeof window.ym !== 'function') return Promise.resolve('');
324      return new Promise((resolve) => {
325        let finished = false;
326        const done = (value = '') => {
327          if (finished) return;
328          finished = true;
329          cachedClientId = String(value || '');
330          resolve(cachedClientId);
331        };
332        window.setTimeout(() => done(''), 1200);
333        try { window.ym(id, 'getClientID', done); } catch (_) { done(''); }
334      });
335    },
336    async getCalltouchSessionID() {
337      if (!window.EduChessConsent?.analytics || !canLoadCalltouch() || typeof window.ct !== 'function') return '';
338
339      const normalize = (data) => String(data?.sessionId || data?.session_id || '');
340      const deadline = Date.now() + 1200;
341      try {
342        // The consent-gated loader first exposes Calltouch's queue shim. Wait
343        // briefly for init-min.js to replace/activate it; otherwise a form sent
344        // immediately after accepting cookies could miss the Calltouch session.
345        while (window.ct?.loaded === false && Date.now() < deadline) {
346          await new Promise((resolve) => window.setTimeout(resolve, 50));
347        }
348        if (typeof window.ct !== 'function') return '';
349
350        // Current Calltouch supports Promise mode and resolves it when the
351        // actual session identifiers are ready. Never delay form submission for
352        // longer than the single 1.2-second attribution budget above.
353        const pending = window.ct('calltracking_params', calltouchModId(), null, 'withPromise');
354        if (pending && typeof pending.then === 'function') {
355          const remaining = Math.max(0, deadline - Date.now());
356          const timeout = new Promise((resolve) => window.setTimeout(() => resolve(null), remaining));
357          const data = await Promise.race([pending, timeout]);
358          const sessionId = normalize(data);
359          if (sessionId) return sessionId;
360        }
361
362        return normalize(window.ct('calltracking_params', calltouchModId()));
363      } catch (_) {
364        return '';
365      }
366    },
367  };
368})();

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.