PageSourceSearch

https://tagankateatr.ru/_inc/footer.js?6756

js tagankateatr.ru collected 2026-10-02 09:58:38 UTC 6,650 bytes, 137 lines download raw bytes

1/* ============================================================
2   FOOTER.JS
3   ============================================================ */
4
5(function () {
6  'use strict';
7
8  // ── Аккордеон «Все разделы» ──
9  const sectionsBtn   = document.getElementById('footerSectionsBtn');
10  const sectionsPanel = document.getElementById('footerSectionsPanel');
11
12  if (sectionsBtn && sectionsPanel) {
13    // Панель схлопнута через grid-template-rows: 0fr + overflow: hidden.
14    // Это скрывает её визуально, но НЕ убирает 27 ссылок из Tab-обхода:
15    // с клавиатуры фокус проваливался внутрь закрытой панели, тогда как
16    // aria-hidden="true" прятал её от скринридера. Атрибут inert снимает
17    // и фокусируемость, и озвучку разом.
18    sectionsPanel.toggleAttribute('inert', sectionsPanel.getAttribute('aria-hidden') !== 'false');
19
20    sectionsBtn.addEventListener('click', () => {
21      const isOpen = sectionsBtn.getAttribute('aria-expanded') === 'true';
22      sectionsBtn.setAttribute('aria-expanded', String(!isOpen));
23      sectionsPanel.setAttribute('aria-hidden',  String(isOpen));
24      sectionsPanel.toggleAttribute('inert', isOpen);
25    });
26  }
27
28  // ── Дисклеймер подписки: появляется при фокусе на email-поле ──
29  const form       = document.getElementById('footerSubscribeForm');
30  const emailInput = document.getElementById('footerEmail');
31  const agreeWrap  = document.getElementById('footerAgreeWrap');
32  const agreeBox   = document.getElementById('footerAgree');
33  const agreeError = document.getElementById('footerAgreeError');
34  const emailBtn   = form ? form.querySelector('.footer__email-btn') : null;
35
36  // На десктопе (≥1024px) CSS показывает обёртку статично через
37  // display: block !important, а aria-hidden="true" из разметки оставался —
38  // чекбокс согласия был виден и фокусируем, но скрыт от скринридера.
39  // Атрибут здесь не нужен вовсе: видимостью управляет display,
40  // а display: none и так убирает блок из дерева доступности и из Tab-обхода.
41  if (agreeWrap) agreeWrap.removeAttribute('aria-hidden');
42
43  function showAgree() {
44    if (!agreeWrap) return;
45    agreeWrap.classList.add('is-visible');
46  }
47
48  function hideAgree() {
49    if (!agreeWrap || !emailInput) return;
50    // Скрываем только если поле пустое — иначе оставляем видимым
51    if (emailInput.value.trim() === '') {
52      agreeWrap.classList.remove('is-visible');
53    }
54  }
55
56  // Подсветка чекбокса + строка ошибки под ним.
57  // Вся визуалка — в CSS (.is-invalid, .footer__agree-error), JS только
58  // переключает класс и атрибутный hidden.
59  function setAgreeError(show) {
60    if (!agreeBox) return;
61    if (agreeError) agreeError.hidden = !show;
62    agreeBox.classList.toggle('is-invalid', show);
63    agreeBox.setAttribute('aria-invalid', show ? 'true' : 'false');
64  }
65
66  if (emailInput) {
67    emailInput.addEventListener('focus', showAgree);
68    emailInput.addEventListener('blur',  hideAgree);
69  }
70
71  // Снимаем ошибку сразу, как только пользователь проставил галочку
72  if (agreeBox) {
73    agreeBox.addEventListener('change', () => {
74      if (agreeBox.checked) setAgreeError(false);
75    });
76  }
77
78  // ── Форма подписки ──
79
80  if (form) {
81    form.addEventListener('submit', (e) => {
82      e.preventDefault();
83      const email  = emailInput ? emailInput.value.trim() : '';
84      const agreed = agreeBox ? agreeBox.checked : true;
85
86      if (!email) { emailInput && emailInput.focus(); return; }
87
88      // Галочка согласия на обработку ПД обязательна и НЕ предзаполнена:
89      // без активного действия пользователя согласия нет (ч. 1 ст. 9 152-ФЗ).
90      if (agreeBox && !agreed) {
91        showAgree();          // на мобилке блок мог быть ещё скрыт
92        setAgreeError(true);
93        agreeBox.focus();
94        return;
95      }
96      setAgreeError(false);
97
98      // Блокируем кнопку на время запроса, чтобы избежать повторной отправки
99      if (emailBtn) emailBtn.disabled = true;
100
101      fetch('/subscribe', {
102        method: 'POST',
103        headers: {
104          'Content-Type': 'application/json'
105        },
106        // agree уходит на бэкенд явным флагом — сервер фиксирует факт согласия
107        // (дату/время, IP, версию текста) как докаÐ
107·Ð°Ñ‚ельство по ч. 1 ст. 18
108        // ФЗ «О рекламе»: бремя доказывания согласия лежит на театре
109        body: JSON.stringify({ email: email, agree: agreed })
110      })
111        .then((response) => {
112          if (!response.ok) {
113            throw new Error('Ошибка сервера: ' + response.status);
114          }
115          return response.json().catch(() => ({})); // на случай пустого тела ответа
116        })
117        .then((data) => {
118          console.log('[Footer] Subscribe success:', email, data);
119          emailInput.value = '';
120          // Double opt-in: подписки ещё нет — она начинается после подтверждения
121          // из письма. «Вы подписаны» здесь было бы неправдой.
122          emailInput.placeholder = 'Спасибо! Проверьте почту';
123          emailInput.disabled = true;
124          // Класс, а не inline-стиль: .footer__email-btn.is-hidden уже есть
125          // в footer.css — у кнопки display:flex, который перебил бы [hidden]
126          if (emailBtn) emailBtn.classList.add('is-hidden');
127          hideAgree();
128        })
129        .catch((error) => {
130          console.error('[Footer] Subscribe error:', error);
131          if (emailBtn) emailBtn.disabled = false;
132          emailInput.placeholder = 'Не получилось, попробуйте ещё раз';
133        });
134    });
135  }
136
137})();

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.