PageSourceSearch

https://tagankateatr.ru/_inc/accessibility-init.js?6756

js tagankateatr.ru collected 2026-10-02 09:58:41 UTC 17,600 bytes, 311 lines download raw bytes

1/**
2 * Инициализация плагина «Версия для слабовидящих» (BVI, ГОСТ Р 52872-2012).
3 *
4 * Это НЕ часть 4-слойной архитектуры проекта — сторонняя вендорная библиотека
5 * (assets/vendor/bvi/), работает одинаково на каждой странице сайта независимо
6 * от CMS-контента, поэтому подключается отдельным слоем поверх всего остального.
7 *
8 * Зависимость: assets/vendor/bvi/js/bvi.min.js должен быть подключён ДО этого файла.
9 *
10 * Триггеры — уже существующие в разметке кнопки, свою верстку плагин НЕ добавляет:
11 *   .footer__btn--access    — кнопка в подвале (одна разметка на мобилку и десктоп,
12 *                              разница только в CSS, см. footer.css)
13 *   .menu__accessibility    — кнопка в мобильном меню
14 *   .menu__desktop-access   — кнопка в десктопном меню
15 * Все три объединены в один селектор target — плагин сам биндит клик на каждую.
16 * Если появится ещё одна кнопка «Версия для слабовидящих» — достаточно
17 * дописать её класс через запятую в target ниже, HTML менять не нужно.
18 *
19 * Библиотека сама строит DOM панели настроек и плавающую иконку возврата
20 * (когда пользователь скрывает панель), хранит выбор пользователя в cookie
21 * вида bvi_* — единственное сознательное исключение из правила проекта
22 * «никакого localStorage/sessionStorage», т.к. настройки должны сохраняться
23 * между переходами по страницам сайта.
24 *
25 * Оформление панели — дефолтное (тема 'white'), без подгонки под токены театра.
26 * Если понадобится фирменная тема — переопределять классы .bvi-* в отдельном
27 * файле (не трогая сам assets/vendor/bvi/css/bvi.min.css).
28 *
29 * Компенсирующий слой: components/accessibility/accessibility.css — чинит то,
30 * что плагин ломает в вёрстке (padding обёртки, плоская типографика, шрифт).
31 * Подключается СТРОГО ПОСЛЕ bvi.min.css.
32 */
33(function () {
34  if (typeof isvek === 'undefined') {
35    console.warn('[accessibility] bvi.min.js не загружен — плагин версии для слабовидящих не инициализирован.');
36    return;
37  }
38
39  new isvek.Bvi({
40    target: '.footer__btn--access, .menu__accessibility, .menu__desktop-access',
41    lang: 'ru-RU',
42    theme: 'white',
43
44    // Стартовый размер. На десктопе дополнительно умножается
45    // коэффициентом --bvi-scale из accessibility.css (см. секцию 6),
46    // потому что вёрстка масштабируется от базы 1680px.
47    fontSize: 18,
48
49    // Изображения в цвете. Дефолт плагина — 'grayscale', то есть
50    // все фото приходили обесцвеченными. Значение 'false' полностью
51    // скрывает изображения — пользователь может выбрать это сам
52    // в панели, но навязывать такое по умолчанию не нужно.
53    images: true,
54
55    speech: true,
56    panelFixed: true,
57    reload: false,
58
59    // Встраиваемые элементы (video, iframe, ymaps) — ВЫКЛЮЧЕНЫ.
60    // В hero фото и видео лежат друг на друге: <img class="slide__image">
61    // снизу, <video class="slide__video"> поверх него проявляется через
62    // 1.25с. Скрывая видео, мы открываем фото под ним — в режиме
63    // слабовидящих движущийся фон не нужен, а картинка нужна.
64    //
65    // Побочный эффект вендора: этот же флаг прячет <figure>, поэтому
66    // в accessibility.css (секция 5) figure принудительно возвращён —
67    // у нас это обёртка обычных изображений, а не только видео.
68    //
69    // Пользователь может включить встраиваемые элементы обратно
70    // в настройках панели.
71    builtElements: false
72  });
73
74  /* ── РАЗМЕРЫ ЧЕРЕЗ ЗУМ ──────────────────────────────────────
75     Плагин назначает абсолютный font-size каждому элементу и тем
76     стирает типографику сайта. Вместо спора о специфичности
77     снимаем атрибут [data-bvi-fontsize] с .bvi-body — без него
78     вендорные правила размеров просто не совпадают, и вёрстка
79     остаётся в своих исходных, читабельных размерах.
80
81     Выбранное значение превращаем в коэффициент зума и кладём
82     в CSS-переменную --bvi-zoom (применяется в accessibility.css).
83     Это единственное место, где скрипт пишет стиль, и пишет он
84     не оформление, а один вычисленный коэффициент.
85
86     BASE = 18 совпадает с fontSize в конфиге выше: при стартовом
87     значении зум равен 1, то есть сайт выглядит ровно как обычно,
88     а кнопки «+/−» в панели увеличивают и уменьшают его.         */
89  var BASE = 18;
90  var MIN = 0.8;
91  var MAX = 2.5;
92  var busy = false;
93
94  function applyZoom(bviBody) {
95    if (busy) return;
96    var raw = bviBody.getAttribute('data-bvi-fontsize');
97    if (raw === null) return;
98
99    var n = parseInt(raw, 10);
100    if (isNaN(n) || n < 1) n = BASE;
101
102    var zoom = Math.min(MAX, Math.max(MIN, n / BASE));
103
104    busy = true;
105    bviBody.style.setProperty('--bvi-zoom', String(zoom));
106    // атрибут снимаем ПОСЛЕ чтения — вендорные правила размеров
107    // перестают совпадать, типографика сайта не переписывается
108    bviBody.removeAttribute('data-bvi-fontsize');
109    busy = false;
110  }
111
112  /* Плагин пересоздаёт разметку при включении режима и
113     проставляет атрибут заново при каждом нажатии «+/−»,
114     поэтому следим за появлением .bvi-body и за атрибутом. */
115  var observer = new MutationObserver(function () {
116    var bviBody = document.querySelector('.bvi-body');
117    if (bviBody) applyZoom(bviBody);
118  });
119
120  observer.observe(document.documentElement, {
121    childList: true,
122    subtree: true,
123    attributes: true,
124    attributeFilter: ['data-bvi-fontsize']
125  });
126
127  // на случай, если режим уже был включён из cookie до запуска
128  var existing = document.querySelector('.bvi-body');
129  if (existing) applyZoom(existing);
130
131  /* ── ЗАКРЫТИЕ МЕНЮ ПРИ ЗАПУСКЕ ИЗ МЕНЮ ─────────────────────
132     Кнопки «Версия для слабовидящих» есть в трёх местах: подвал,
133     мобильное меню и десктопное меню. При запуске из меню оно
134     должно закрываться — иначе панель плагина открывается позади
135     развёрнутого меню, и пользователь её не видит.
136
137     Меню не отдаёт свой экземпляр наружу, поэтому вызвать
138     menuInstance.close() напрямую нельзя. Зато menu.js вешает
139     обработчик на все кнопки с классом .menu__close (мобильная,
140     десктопная и кнопка второго уровня) — кликаем по видимой,
141     и отрабатывает штатное закрытие целиком: снятие блокировки
142     скролла, возврат фокуса, закрытие подменю.
143
144     Так мы не дублируем логику и не трогаем menu.js —
145     при его изменении здесь ничего чинить не придётся.        */
146  var menuTriggers = document.querySelectorAll('.menu__accessibility, .menu__desktop-access');
147
148  Array.prototype.forEach.call(menuTriggers, function (trigger) {
149    trigger.addEventListener('click', function () {
150      var menu = document.getElementById('mainMenu');
151      if (!menu || !menu.classList.contains('is-open')) return;
152
153      // берём кнопку закрытия, видимую на текущем брейкпоинте
154      var buttons = menu.querySelectorAll('.menu__close');
155      for (var i = 0; i < buttons.length; i++) {
156        if (buttons[i].offsetParent !== null) {
157          buttons[i].click();
158          return;
159        }
160      }
161
162      // запасной вариант, если ни одна кнопка не отрисована
163      if (buttons.length) buttons[0].click();
164    });
165  });
166
167  /* ── СДВИГ ПОЛНОЭКРАННЫХ СЛОЁВ ПОД ПАНЕЛЬ ──────────────────
168     Панель плагина — position:fixed; top:0; z-index:999999, то есть
169     выше любого нашего слоя (у меню --z-modal, это 200). Меню
170     открывается во весь экран от top:0 и уезжает под панель
171     вместе с кнопкой закрытия — выйти из меню становится нечем.
172
173     Поднимать меню выше панели нельзя: тогда недоступной станет
174     сама панель. Поэтому измеряем её высоту и сдвигаем на неё
175     полноэкранные слои (меню, десктопный поиск) — оба остаются
176     полностью рабочими.
177
178     Высота панели зависит от ширины экрана и от того, свернул ли
179     её пользователь, поэтому пересчитываем на resize и при любых
180     изменениях внутри самой панели.                             */
181  function updatePanelOffset() {
182    var panel = document.querySelector('.bvi-panel');
183    var visible = panel && !panel.classList.contains('bvi-panel-hide');
184
185    /* Сдвиг нужен, только когда панель ПЛАВАЮЩАЯ (position:fixed):
186       именно тогда она ложится поверх содержимого. Если панель
187       не закреплена, она стоит в потоке и сама раздвигает страницу —
188       дополнительный сдвиг тогда лишь создал бы пустую полосу сверху. */
189    var floating = false;
190    if (visible) {
191      floating = window.getComputedStyle(panel).position === 'fixed';
192    }
193
194    var height = floating ? panel.getBoundingClientRect().height : 0;
195
196    document.documentElement.style.setProperty('--bvi-panel-h', Math.round(height) + 'px');
197  }
198
199  /* Панель закрепляется НЕ сразу: плагин вешает .bvi-fixed-top
200     только после прокрутки страницы на 200px, до этого панель
201     стоит в потоке. Поэтому слушаем скролл — именно он меняет
202     режим — и пересчитываем сдвиг.
203
204     Скролл троттлим через requestAnimationFrame, как принято
205     в проекте: замер вызывает getComputedStyle и getBoundingClientRect,
206     на каждый пиксель прокрутки это дорого.                     */
207  var ticking = false;
208
209  function requestUpdate() {
210    if (ticking) return;
211    ticking = true;
212    requestAnimationFrame(function () {
213      updatePanelOffset();
214      ticking = false;
215    });
216  }
217
218  window.addEventListener('scroll', requestUpdate, { passive: true });
219
220  var resizeTimer = null;
221  window.addEventListener('resize', function () {
222    clearTimeout(resizeTimer);
223    resizeTimer = setTimeout(function () {
224      updatePanelOffset();
225      adjustCloseLabel();
226    }, 150);
227  });
228
229  /* Панель создаётся плагином не сразу. Ждём её появления, после
230     чего наблюдаем ТОЛЬКО за ней, а не за всем документом:
231     общий наблюдатель срабатывал бы на каждое переключение
232     is-active/is-open на сайте и зря дёргал бы пересчёт.        */
233  /* Кнопки сворачивания и возврата панели состоят из одной
234     иконки, без текста — программы чтения с экрана объявляют
235     их просто «ссылка». Проставляем доступные имена.
236
237     Подпись «Скрыть панель» на мобильных рисуется через ::after
238     в CSS; сгенерированный контент читают не все программы,
239     поэтому aria-label здесь не дублирование, а основа.       */
240  /* На узких экранах «Обычная версия сайта» занимает почти всю
241     строку. Сокращаем до «Обычная версия».
242
243     Текст приходит из локализации плагина, поэтому меняем его
244     скриптом, а не CSS: подмена через ::after сломала бы
245     доступное имя ссылки. Оригинал сохраняем в data-атрибуте
246     и возвращаем на десктопе.                                 */
247  var MOBILE_QUERY = '(max-width: 1023.98px)';
248
249  function adjustCloseLabel() {
250    var link = document.querySelector('.bvi-panel a[data-bvi="close"]');
251    if (!link) return;
252
253    if (!link.getAttribute('data-taganka-full-label')) {
254      link.setAttribute('data-taganka-full-label', link.textContent.trim());
255    }
256
257    var full = link.getAttribute('data-taganka-full-label');
258    var isMobile = window.matchMedia(MOBILE_QUERY).matches;
259    var next = isMobile ? 'Обычная версия' : full;
260
261    if (link.textContent.trim() !== next) {
262      link.textContent = next;
263    }
264  }
265
266  function labelIconButtons() {
267    var hide = document.querySelector('.bvi-panel a[data-bvi="panel-hide"]');
268    var show = document.querySelector('.bvi-link-fixed-top');
269
270    if (hide && !hide.getAttribute('aria-label')) {
271      hide.setAttribute('aria-label', 'Скрыть панель настроек');
272    }
273
274    /* Подпись «Скрыть панель» рядом с кружком.
275
276       Вставляем реальный <span>, а не рисуем через CSS ::after:
277       псевдоэлемент вёл себя непредсказуемо в раскладке панели,
278       а настоящий элемент занимает своё место в потоке. Видимость
279       по брейкпоинтам управляется классом в accessibility.css —
280       на десктопе подпись скрыта, там кнопка круглая. */
281    if (hide && !hide.querySelector('.taganka-hide-label')) {
282      var label = document.createElement('span');
283      label.className = 'taganka-hide-label';
284      label.textContent = 'Скрыть панель';
285      hide.appendChild(label);
286    }
287
288    if (show && !show.getAttribute('aria-label')) {
289      show.setAttribute('aria-label', 'Показать панель настроек');
290    }
291  }
292
293  var appearObserver = new MutationObserver(function () {
294    var panel = document.querySelector('.bvi-panel');
295    if (!panel) return;
296
297    appearObserver.disconnect();
298    updatePanelOffset();
299    labelIconButtons();
300    adjustCloseLabel();
301
302    new MutationObserver(requestUpdate).observe(panel, {
303      attributes: true,
304      attributeFilter: ['class']
305    });
306  });
307
308  appearObserver.observe(document.body, { childList: true, subtree: true });
309
310  updatePanelOffset();
311})();

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.