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.