PageSourceSearch

https://tagankateatr.ru/_inc/cookie-consent.js?6794

js tagankateatr.ru collected 2026-10-02 09:59:18 UTC 12,561 bytes, 305 lines download raw bytes

1/* ══════════════════════════════════════════════════════════════
2   COOKIE-CONSENT — модуль уведомления / согласия на cookie
3   Театр на Таганке
4
5   window.CookieConsent — публичный API для подключения будущих
6   скриптов без правок ядра модуля.
7
8   ЧТО ДЕЛАЕТ МОДУЛЬ:
9     — читает cookie выбора и отдаёт его состояние наружу;
10     — пишет cookie при нажатии кнопок;
11     — при отказе гасит Consent Mode, чистит свои cookie аналитики
12       и перезагружает страницу;
13     — переоткрывает плашку по кнопке в подвале.
14
15   ЧЕГО МОДУЛЬ НЕ ДЕЛАЕТ:
16     — не решает, показывать ли плашку при первой загрузке:
17       это делает сервер, добавляя класс .is-visible при отсутствии
18       cookie (вариант A — полностраничного кэша нет);
19     — не генерирует разметку: вся вёрстка приходит из шаблона CMS;
20     — не пишет inline-стили: только классы is-* и атрибуты.
21   ══════════════════════════════════════════════════════════════ */
22
23(function () {
24  'use strict';
25
26  var COOKIE_NAME    = 'theatre_cookie_consent';
27  var COOKIE_VERSION = 1;
28  var MAX_AGE        = 31536000;   /* 12 месяцев в секундах */
29
30  /* Cookie аналитики, доступные сайту на своём домене.
31     Cookie сторонних доменов (.mc.yandex.ru и подобные) удалить
32     средствами сайта невозможно — это ограничение описано в политике. */
33  var ANALYTICS_COOKIE_NAMES    = ['_ga', '_gid', '_gat', '_ym_uid', '_ym_d', '_ym_isad', '_ym_visorc'];
34  var ANALYTICS_COOKIE_PREFIXES = ['_ga_', '_gat_', '_ym_'];
35
36  var root      = null;
37  var mode      = 'consent';
38  var state     = null;
39  var listeners = [];
40  var lastTrigger = null;
41
42
43  /* ────────────────────────────────────────────────
44     РАБОТА С COOKIE
45     ──────────────────────────────────────────────── */
46
47  /* Домен верхнего уровня, чтобы выбор действовал и на
48     tagankateatr.ru, и на m.tagankateatr.ru.
49     Можно переопределить атрибутом data-consent-domain. */
50  function cookieDomain() {
51    if (root && root.dataset.consentDomain) return root.dataset.consentDomain;
52    var parts = location.hostname.split('.');
53    if (parts.length < 2) return '';               /* localhost — host-only */
54    return '.' + parts.slice(-2).join('.');
55  }
56
57  function readRaw(name) {
58    var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
59    return match ? decodeURIComponent(match[1]) : null;
60  }
61
62  function writeState(next) {
63    var domain = cookieDomain();
64    var value  = encodeURIComponent(JSON.stringify(next));
65    document.cookie = COOKIE_NAME + '=' + value +
66      ';path=/' +
67      (domain ? ';domain=' + domain : '') +
68      ';max-age=' + MAX_AGE +
69      ';SameSite=Lax' +
70      (location.protocol === 'https:' ? ';Secure' : '');
71    state = next;
72  }
73
74  function readState() {
75    var raw = readRaw(COOKIE_NAME);
76    if (!raw) return null;
77    try {
78      var parsed = JSON.parse(raw);
79      if (!parsed || parsed.v !== COOKIE_VERSION) return null;
80      return parsed;
81    } catch (e) {
82      return null;
83    }
84  }
85
86  function dropCookie(name) {
87    var domain = cookieDomain();
88    var paths  = ['/', location.pathname];
89    var hosts  = ['', location.hostname, domain];
90    for (var p = 0; p < paths.length; p++) {
91      for (var h = 0; h < hosts.length; h++) {
92        document.cookie = name + '=;path=' + paths[p] +
93          (hosts[h] ? ';domain=' + hosts[h] : '') +
94          ';max-age=0';
95      }
96    }
97  }
98
99  function clearAnalyticsCookies() {
100    var all = document.cookie.split(';');
101    for (var i = 0; i < all.length; i++) {
102      var name = all[i].split('=')[0].trim();
103      if (!name) continue;
104
105      var match = ANALYTICS_COOKIE_NAMES.indexOf(name) !== -1;
106      if (!match) {
107        for (var p = 0; p < ANALYTICS_COOKIE_PREFIXES.length; p++) {
108          if (name.indexOf(ANALYTICS_COOKIE_PREFIXES[p]) === 0) { match = true; break; }
109        }
110      }
111      if (match) dropCookie(name);
112    }
113  }
114
115
116  /* ────────────────────────────────────────────────
117     СОСТОЯНИЕ И ПОДПИСКИ
118     ──────────────────────────────────────────────── */
119
120  function categories() {
121    return {
122      necessary: true,
123      analytics: state ? state.analytics === true : false
124    };
125  }
126
127  function notify() {
128    var value = categories();
129    for (var i = 0; i < listeners.length; i++) {
130      try { listeners[i](value); } catch (e) { /* подписчик не должен ломать модуль */ }
131    }
132  }
133
134  /* Google Consent Mode v2. Дефолт (granted) выставляется в шаблоне
135     до инициализации GTM — здесь только реакция на выбор. */
136  function pushConsentMode(granted) {
137    if (typeof window.gtag === 'function') {
138      window.gtag('consent', 'update', {
139        analytics_storage:  granted ? 'granted' : 'denied',
140        ad_storage:         'denied',
141        ad_user_data:       'denied',
142        ad_personalization: 'denied'
143      });
144    }
145    window.dataLayer = window.dataLayer || [];
146    window.dataLayer.push({
147      event: 'cookie_consent_update',
148      consent: { analytics: granted }
149    });
150  }
151
152
153  /* ────────────────────────────────────────────────
154     ПОКАЗ И СКРЫТИЕ ПЛАШКИ
155     ──────────────────────────────────────────────── */
156
157  function show(trigger) {
158    if (!root) return;
159    lastTrigger = trigger || null;
160    root.classList.add('is-visible');
161    root.removeAttribute('aria-hidden');
162  }
163
164  function hide() {
165    if (!root) return;
166
167    /* Снять фокус с потомка до aria-hidden — правило проекта,
168       иначе браузер ругается на focused descendant. */
169    if (root.contains(document.activeElement)) document.activeElement.blur();
170
171    root.classList.remove('is-visible');
172    root.setAttribute('aria-hidden', 'true');
173
174    if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
175    lastTrigger = null;
176  }
177
178
179  /* ────────────────────────────────────────────────
180     ОБРАБОТЧИКИ КНОПОК
181     ──────────────────────────────────────────────── */
182
183  function record(analytics) {
184    writeState({
185      v: COOKIE_VERSION,
186      mode: mode,
187      analytics: mode === 'notice' ? null : analytics,
188      ts: new Date().toISOString()
189    });
190  }
191
192  function onAccept() {
193    record(true);
194    pushConsentMode(true);
195    hide();
196    notify();
197  }
198
199  function onDecline() {
200    record(false);
201    pushConsentMode(false);
202    clearAnalyticsCookies();
203    hide();
204    notify();
205    /* Перезагрузка выгружает уже работающие счётчики.
206       На следующем рендере сервер их не подставит. */
207    location.reload();
208  }
209
210  /* Крестик (только режим notice): ничего не сохраняет,
211     плашка вернётся при следующем визите. */
212  function onClose() {
213    hide();
214  }
215
216  function onKeydown(e) {
217    if (e.key !== 'Escape') return;
218    if (!root || !root.classList.contains('is-visible')) return;
219    /* В полном режиме Esc не закрывает — это был бы обход выбора. */
220    if (mode === 'notice') onClose();
221  }
222
223
224  /* ────────────────────────────────────────────────
225     ИНИЦИАЛИЗАЦИЯ
226     ──────────────────────────────────────────────── */
227
228  function init() {
229    root = document.querySelector('.cookie-consent');
230    if (!root) return;
231
232    mode  = root.dataset.consentMode === 'notice' ? 'notice' : 'consent';
233    state = readState();
234
235    /* Страховка на случай смены режима notice → consent:
236       выбор, сделанный в режиме уведомления, не засчитывается
237       как согласие. Основную проверку делает сервер, эта —
238       на случай рассинхронизации шаблонов между хостами. */
239    if (state && mode === 'consent' && state.mode === 'notice') {
240      show();
241    }
242
243    var accept  = root.querySelector('.cookie-consent__btn--accept');
244    var decline = root.querySelector('.cookie-consent__btn--decline');
245    var close   = root.querySelector('.cookie-consent__close');
246
247    if (accept)  accept.addEventListener('click', onAccept);
248    if (decline) decline.addEventListener('click', onDecline);
249    if (close)   close.addEventListener('click', onClose);
250
251    document.addEventListener('keydown', onKeydown);
252
253    /* Кнопка «Настройки cookie» в подвале — может быть не одна
254       (в футере копирайт продублирован для десктопа и мобилки). */
255    var openers = document.querySelectorAll('[data-cookie-settings]');
256    for (var i = 0; i < openers.length; i++) {
257      openers[i].addEventListener('click', function (e) { show(e.currentTarget); });
258    }
259
260    window.CookieConsent.destroy = function () {
261      if (accept)  accept.removeEventListener('click', onAccept);
262      if (decline) decline.removeEventListener('click', onDecline);
263      if (close)   close.removeEventListener('click', onClose);
264      document.removeEventListener('keydown', onKeydown);
265      listeners = [];
266    };
267  }
268
269
270  /* ────────────────────────────────────────────────
271     ПУБЛИЧНЫЙ API
272     ──────────────────────────────────────────────── */
273
274  window.CookieConsent = {
275    /* Текущее состояние категорий: { necessary, analytics } */
276    get: function () { return categories(); },
277
278    /* Разрешена ли категория: CookieConsent.has('analytics') */
279    has: function (category) { return categories()[category] === true; },
280
281    /* Подписка на изменение выбора. Возвращает функцию отписки. */
282    onChange: function (fn) {
283      if (typeof fn !== 'function') return function () {};
284      listeners.push(fn);
285      return function () {
286        var i = listeners.indexOf(fn);
287        if (i !== -1) listeners.splice(i, 1);
288      };
289    },
290
291    /* Открыть плашку заново (кнопка в подвале) */
292    open: function () { show(); },
293
294    /* Сделан ли выбор вообще */
295    isAnswered: function () { return state !== null; },
296
297    destroy: function () {}
298  };
299
300  if (document.readyState === 'loading') {
301    document.addEventListener('DOMContentLoaded', init);
302  } else {
303    init();
304  }
305})();

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.