PageSourceSearch

https://www.dab.sk/assets/a11y.js?v3.3.5

js dab.sk collected 2026-10-02 19:19:16 UTC 30,499 bytes, 758 lines download raw bytes

1/*
2 * Opravy přístupnosti podle auditu MIRRI SR (28.–31. 8. 2026).
3 *
4 * Načítá se za frontend.js (META-JS.php) a necompiluje se webpackem — build
5 * frontendu na současném Node nejde spustit. Při upgradu stacku patří do
6 * app/assets/js. ES5 + jQuery (globální z frontend.js).
7 */
8(function ($) {
9    'use strict';
10
11    var MOBILE = '(max-width: 767px)';
12
13    function isMobile() {
14        return window.matchMedia && window.matchMedia(MOBILE).matches;
15    }
16
17    function isHovered(el) {
18        try {
19            return el.matches ? el.matches(':hover') : false;
20        } catch (e) {
21            return false;
22        }
23    }
24
25    function isEscape(e) {
26        return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
27    }
28
29    /*
30     * Hlavní menu — KS 2.1.1, 2.5.1, 1.4.13.
31     *
32     * Podmenu se dosud otevíralo jen přes CSS :hover: klávesnicí nešlo otevřít,
33     * nešlo zavřít jinak než odjetím myší a při vychýlení z dráhy se hned zavřelo.
34     * Každá rozbalovací položka teď dostane tlačítko (aria-expanded), které
35     * podmenu otevírá klikem i klávesnicí; Esc zavírá. Myší zůstává otevírání
36     * najetím, ale se zpožděním při odjetí a s možností podmenu klikem připnout.
37     */
38    function initMenu() {
39        var $menu = $('#menu');
40        var CLOSE_DELAY = 400;
41        var $items;
42
43        if (!$menu.length) {
44            return;
45        }
46
47        $items = $menu.children('li.dropdown').filter(function () {
48            return $(this).children('.dropdown-menu').length > 0;
49        });
50
51        $items.each(function (i) {
52            var $li = $(this);
53            var $link = $li.children('a').first();
54            var $sub = $li.children('.dropdown-menu').first();
55            var id = $sub.attr('id') || ('podmenu-' + (i + 1));
56
57            $sub.attr('id', id);
58            $link.after(
59                $('<button type="button" class="submenu-toggle" aria-expanded="false"></button>')
60                    .attr('aria-controls', id)
61                    .append($('<span class="sr-only"></span>').text('Podmenu: ' + $.trim($link.text())))
62            );
63        });
64
65        // Styly v a11y.css vypínají CSS :hover jen pod touto třídou — bez JS
66        // zůstane menu fungovat postaru.
67        $menu.addClass('a11y-menu');
68
69        // Na mobilu rozbaloval podmenu klik na nadřazený odkaz (functions.js).
70        // Tuto roli teď má tlačítko, odkaz normálně vede na svou stránku.
71        // Platí i pro li.dropdown bez vypsaného podmenu (MENU.php dává třídu
72        // i položce, jejíž podpoložky jsou všechny skryté) — jinak by handler
73        // s return false nechal odkaz na mobilu nefunkční.
74        $menu.children('li.dropdown').children('a').off('click');
75
76        function setOpen($li, open, via) {
77            clearTimeout($li.data('a11yTimer'));
78            $li.toggleClass('is-open', open)
79                .data('a11yVia', open ? via : null)
80                .children('.submenu-toggle').attr('aria-expanded', open ? 'true' : 'false');
81            if (open) {
82                placeDropdown($li);
83            }
84        }
85
86        // KS 1.4.4, 1.4.10, 1.4.12: při přiblížení nebo větších rozestupech podmenu
87        // (hlavně „O divadle" s dlouhými položkami) vyjíždělo vpravo mimo obrazovku.
88        // Nejdřív ho posunout doleva; teprve když je širší než obrazovka, omezit
89        // šířku a nechat položky zalomit. U fixní hlavičky omezit i výšku, jinak
90        // by se spodek podmenu nedal doscrollovat.
91        function placeDropdown($li) {
92            var ul = $li.children('.dropdown-menu')[0];
93            var gap = 8;
94            var vw, r, li, avail;
95
96            if (!ul) {
97                return;
98            }
99            ul.style.left = '';
100            ul.style.maxWidth = '';
101            ul.style.maxHeight = '';
102            $(ul).removeClass('is-constrained');
103            if (isMobile()) {
104                return;
105            }
106            vw = document.documentElement.clientWidth;
107            li = $li[0].getBoundingClientRect();
108            r = ul.getBoundingClientRect();
109            if (r.width > vw - 2 * gap) {
110                $(ul).addClass('is-constrained');
111                ul.style.maxWidth = (vw - 2 * gap) + 'px';
112                r = ul.getBoundingClientRect();
113            }
114            if (r.right > vw - gap) {
115                ul.style.left = Math.round(vw - gap - r.width - li.left) + 'px';
116                r = ul.getBoundingClientRect();
117            }
118            if (r.left < gap) {
119                ul.style.left = Math.round(gap - li.left) + 'px';
120                r = ul.getBoundingClientRect();
121            }
122            if ($('#pageHeader').hasClass('fixed')) {
123                avail = window.innerHeight - r.top - gap;
124                if (r.height > avail) {
125                    $(ul).addClass('is-constrained');
126                    ul.style.maxHeight = Math.max(120, Math.floor(avail)) + 'px';
127                }
128            }
129        }
130
131        $(window).on('resize', function () {
132            $items.filter('.is-open').each(function () {
133                placeDropdown($(this));
134            });
135        });
136
137        function closeAll($except) {
138            $items.not($except || $()).filter('.is-open').each(function () {
139                setOpen($(this), false);
140            });
141        }
142
143        $items.on('click', '.submenu-toggle', function () {
144            var $li = $(this).closest('li');
145
146            // Otevřeno najetím myší (a na dotykovém zařízení i tapem) — klik
147            // podmenu připne, nezavře.
148            if ($li.hasClass('is-open') && $li.data('a11yVia') === 'hover') {
149                $li.data('a11yVia', 'click');
150                return;
151            }
152            closeAll($li);
153            setOpen($li, !$li.hasClass('is-open'), 'click');
154        });
155
156        $items.on('mouseenter', function () {
157            var $li = $(this);
158
159            if (isMobile()) {
160                return;
161            }
162            clearTimeout($li.data('a11yTimer'));
163            if ($li.data('a11yDismissed') || $li.hasClass('is-open')) {
164                return;
165            }
166            closeAll($li);
167            setOpen($li, true, 'hover');
168        });
169
170        $items.on('mouseleave', function () {
171            var $li = $(this);
172
173            $li.data('a11yDismissed', false);
174            if (isMobile() || $li.data('a11yVia') !== 'hover') {
175                return;
176            }
177            $li.data('a11yTimer', setTimeout(function () {
178                setOpen($li, false);
179            }, CLOSE_DELAY));
180        });
181
182        // Zaměření odešlo mimo položku — zavřít (kromě otevření myší, to řeší mouseleave).
183        $items.on('focusout', function () {
184            var li = this;
185
186            setTimeout(function () {
187                var $li = $(li);
188                if ($li.hasClass('is-open') && $li.data('a11yVia') !== 'hover' && !$.contains(li, document.activeElement)) {
189                    setOpen($li, false);
190                }
191            }, 0);
192        });
193
194        $(document).on('keydown', function (e) {
195            if (!isEscape(e)) {
196                return;
197            }
198            $items.filter('.is-open').each(function () {
199                var $li = $(this);
200                var hadFocus = $.contains(this, document.activeElement);
201
202                setOpen($li, false);
203                // Zůstává-li kurzor nad položkou, nesmí se hned zase otevřít.
204                $li.data('a11yDismissed', isHovered(this));
205                if (hadFocus) {
206                    $li.children('.submenu-toggle').focus();
207                }
208            });
209        });
210
211        $(document).on('click', function (e) {
212            if (!$(e.target).closest('#menu').length) {
213                closeAll();
214            }
215        });
216
217        if (window.matchMedia) {
218            var mq = window.matchMedia(MOBILE);
219            var onChange = function () { closeAll(); };
220            if (mq.addEventListener) {
221                mq.addEventListener('change', onChange);
222            } else if (mq.addListener) {
223                mq.addListener(onChange);
224            }
225        }
226    }
227
228    /*
229     * Hamburger tlačítko — KS 4.1.2. Stav otevření se oznamuje přes aria-expanded
230     * (název je stálý „Hlavné menu", viz LOGO.php). Menu přepíná functions.js,
231     * tady se jen po jeho handleru synchronizuje stav.
232     */
233    function initNavToggle() {
234        var $btn = $('.navbar-toggle');
235        var $nav = $('#pageHeader .navbar');
236
237        if (!$btn.length || !$nav.length) {
238            return;
239        }
240        $btn.attr('aria-expanded', $nav.is(':visible') && isMobile() ? 'true' : 'false');
241        $btn.on('click', function () {
242            $btn.attr('aria-expanded', $nav.is(':visible') ? 'true' : 'false');
243        });
244    }
245
246    /*
247     * Odsazení obsahu pod hlavičkou — KS 1.4.4, 1.4.12.
248     * Hlavička je position: absolute a obsah odsazuje pevný padding-top <body>
249     * (97 px). Když hlavička při zvětšení textu nebo rozestupů naroste (menu se
250     * zalomí), přikryje drobečkovou navigaci i nadpis stránky — nález auditu
251     * na /subor. Odsazení teď sleduje skutečnou výšku hlavičky; jen na
252     * podstránkách (na úvodní stránce leží hlavička záměrně přes carousel)
253     * a mimo kompaktní režim (.fixed), kdy hlavička z toku vypadne.
254     * Na mobilu je hlavička statická, v toku stránky: tam se neodsazuje nic,
255     * jinak by padding odsunul dolů i hlavičku a po rozbalení hamburgeru by
256     * nad ní zůstala bílá plocha o výšce celého menu.
257     */
258    function initHeaderOffset() {
259        var header = document.getElementById('pageHeader');
260        var body = document.body;
261
262        if (!header || body.id !== 'subpage') {
263            return;
264        }
265
266        function update() {
267            var fromCss, height, position;
268
269            if ($(header).hasClass('fixed')) {
270                return;
271            }
272            body.style.paddingTop = '';
273            position = window.getComputedStyle(header).position;
274            if (position !== 'absolute' && position !== 'fixed') {
275                return;
276            }
277            fromCss = parseFloat(window.getComputedStyle(body).paddingTop) || 0;
278            height = header.offsetHeight;
279            if (height > fromCss + 1) {
280                body.style.paddingTop = height + 'px';
281            }
282        }
283
284        if (window.ResizeObserver) {
285            new window.ResizeObserver(update).observe(header);
286        }
287        $(window).on('resize load', update);
288        update();
289    }
290
291    /*
292     * Kompaktní hlavička — KS 2.4.3, 2.4.7.
293     * Po odscrollování je hlavička fixní a schovaná nad obrazovkou (top: -100px);
294     * vyjede jen při scrollu nahoru (functions.js, hideNav). Přijde-li do ní
295     * zaměření klávesnicí, musí vyjet, jinak je zaměřený prvek neviditelný.
296     */
297    function initHeaderFocus() {
298        var $header = $('#pageHeader');
299
300        $header.on('focusin', function () {
301            if ($header.hasClass('fixed') && !$header.hasClass('showNav')) {
302                $header.addClass('showNav').removeClass('hideNav');
303            }
304        });
305    }
306
307    /*
308     * Měsíční posuvník v programu — KS 2.4.3, 2.1.1, 4.1.2.
309     * V pořadí Tabu bylo všech ~130 měsíců od roku 2015, i když většina leží
310     * mimo viditelnou část posuvníku; šipky byly <a> bez href a bez textu,
311     * takže klávesnicí nešly vůbec. Posuvník se při změně měsíce přestavuje
312     * (functions.js, programMonthsSlider), proto se úpravy aplikují znovu
313     * po každé přestavbě.
314     */
315    function initProgramMonths() {
316        var box = document.getElementById('contentBox');
317        var focusedMonth = null;
318        var pending;
319
320        if (!box || !document.getElementById('programMonths')) {
321            return;
322        }
323
324        // Tab jen na měsíce ve viditelné části posuvníku.
325        function syncFocusable() {
326            var $slider = $('#programMonths');
327            var inner = $slider.find('.inner')[0];
328            var frame;
329
330            if (!inner) {
331                return;
332            }
333            frame = inner.getBoundingClientRect();
334            $slider.find('.item').each(function () {
335                var r = this.getBoundingClientRect();
336                var visible = r.width > 0 && r.left >= frame.left - 1 && r.right <= frame.right + 1;
337                if (visible) {
338                    this.removeAttribute('tabindex');
339                } else {
340                    this.setAttribute('tabindex', '-1');
341                }
342            });
343            $slider.find('.navigation').each(function () {
344                $(this).attr('aria-disabled', $(this).hasClass('disabled') ? 'true' : 'false');
345            });
346        }
347
348        function enhance() {
349            var $slider = $('#programMonths');
350            var api = $slider.data('scrollable');
351            var $active;
352
353            // Přepočet po každém posunu. onSeek přijde až po dojetí animace;
354            // api vzniká znovu při každé přestavbě posuvníku.
355            if (api && api.onSeek && !api.a11yBound) {
356                api.onSeek(syncFocusable);
357                api.a11yBound = true;
358            }
359
360            $slider.find('.navigation.prev').attr({ role: 'button', tabindex: '0', 'aria-label': 'Predchádzajúce mesiace' });
361            $slider.find('.navigation.next').attr({ role: 'button', tabindex: '0', 'aria-label': 'Nasledujúce mesiace' });
362            $slider.find('.item').removeAttr('aria-current');
363            $active = $slider.find('.item.active').attr('aria-current', 'true');
364            syncFocusable();
365
366            // Přestavba posuvníku zahodí zaměřený měsíc a zaměření spadne na body —
367            // vrátit ho na stejný (nebo aktivní) měsíc. Jen v tomto případě, jinak
368            // by jiné AJAXové změny v #contentBox zaměření kradly.
369            if (focusedMonth && !document.body.contains(focusedMonth) &&
370                    (!document.activeElement || document.activeElement === document.body)) {
371                var target = $slider.find('.item[data-month="' + $(focusedMonth).attr('data-month') + '"]')[0] || $active[0];
372                focusedMonth = null;
373                if (target) {
374                    target.focus();
375                }
376            }
377        }
378
379        $(document).on('keydown', '#programMonths .navigation', function (e) {
380            if (e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32) {
381                e.preventDefault();
382                $(this).trigger('click');
383            }
384        });
385        $(document).on('focusin', function (e) {
386            focusedMonth = $(e.target).is('#programMonths .item') ? e.target : null;
387        });
388        $(window).on('resize', function () {
389            setTimeout(syncFocusable, 100);
390        });
391
392        if (window.MutationObserver) {
393            new MutationObserver(function () {
394                clearTimeout(pending);
395                pending = setTimeout(enhance, 50);
396            }).observe(box, { childList: true, subtree: true });
397        }
398        // functions.js posune posuvník na aktivní měsíc až po načtení.
399        setTimeout(enhance, 500);
400        enhance();
401    }
402
403    /*
404     * Programový filtr v postranním panelu — KS 2.4.3, 2.4.7, 4.1.2, 3.3.2.
405     * Odkazy „Žáner", „Autor"… otevírají panel, který je v zavřeném stavu jen
406     * .sr-only: vizuálně skrytý, ale jeho ovládací prvky zůstávaly v pořadí
407     * Tabu. Zavřené panely jsou teď inert, odkaz nese aria-expanded, křížek
408     * je tlačítko a Esc panel zavře. Stav přepíná functions.js třídami,
409     * tady se jen synchronizuje.
410     */
411    function initProgramFilter() {
412        var $menu = $('#programMenu');
413        var $items = $menu.find('> .list > .item').filter(function () {
414            return $(this).children('.programMenu').length > 0;
415        });
416
417        if (!$items.length) {
418            return;
419        }
420
421        function sync() {
422            $items.each(function () {
423                var $item = $(this);
424                var $panel = $item.children('.programMenu');
425                var open = !$panel.hasClass('sr-only');
426
427                $item.children('a').attr('aria-expanded', open ? 'true' : 'false');
428                if (open) {
429                    $panel.removeAttr('inert');
430                } else {
431                    // functions.js zavírá panel i sám po výběru hodnoty — zaměření
432                    // uvnitř by s inert spadlo na <body>, vrátit ho na odkaz filtru.
433                    if ($.contains($panel[0], document.activeElement)) {
434                        $item.children('a').focus();
435                    }
436                    $panel.attr('inert', '');
437                }
438            });
439        }
440
441        $items.each(function (i) {
442            var $item = $(this);
443            var $link = $item.children('a');
444            var $panel = $item.children('.programMenu');
445            var id = $panel.attr('id') || ('programovy-filter-' + (i + 1));
446
447            $panel.attr('id', id);
448            $link.attr('aria-controls', id);
449            $panel.find('.closeBtn').attr({ role: 'button', tabindex: '0', 'aria-label': 'Zavrieť' });
450            // Pole pluginu chosen (autor, herec) nemají popisek — název filtru.
451            $panel.find('.chosen-container input, select').attr('aria-label', $.trim($link.text()));
452        });
453
454        $menu.on('keydown', '.closeBtn', function (e) {
455            if (e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32) {
456                e.preventDefault();
457                var $link = $(this).closest('.item').children('a');
458                $(this).trigger('click');
459                $link.focus();
460            }
461        });
462
463        $menu.on('keydown', '.programMenu', function (e) {
464            if (!isEscape(e)) {
465                return;
466            }
467            var $item = $(this).closest('.item');
468            // Esc v otevřeném seznamu chosen zavře nejdřív seznam.
469            if ($(this).find('.chosen-with-drop').length) {
470                return;
471            }
472            $(this).find('.closeBtn').trigger('click');
473            $item.children('a').focus();
474        });
475
476        if (window.MutationObserver) {
477            new MutationObserver(sync).observe($menu[0], { attributes: true, attributeFilter: ['class'], subtree: true });
478        }
479        sync();
480
481        // Mobilní přepínač celého filtru (≤ 991 px).
482        var $toggle = $('#programMenu-toggle');
483        if ($toggle.length) {
484            $toggle.attr({ 'aria-controls': 'programMenu', role: 'button' });
485            var syncToggle = function () {
486                $toggle.attr('aria-expanded', $menu.is(':visible') ? 'true' : 'false');
487            };
488            $toggle.on('click', function () { setTimeout(syncToggle, 0); });
489            syncToggle();
490        }
491    }
492
493    /*
494     * Úvodní carousel — KS 2.1.1, 4.1.2.
495     * Náhledy pod bannerem přepínají prezentaci, ale byly to <div>y ovladatelné
496     * jen myší (slick, focusOnSelect). Viditelné náhledy jsou teď tlačítka
497     * s názvem podujatí (data-title z HOMEPAGE-CAROUSEL.php) a aria-current.
498     * Slick při každém posunu přepisuje tabindex/aria-hidden, proto se stav
499     * obnovuje po jeho událostech. Klony z nekonečného režimu zůstávají skryté.
500     */
501    function initCarousel() {
502        var $main = $('#hpCarousel');
503        var $nav = $('#carouselNav .inner');
504
505        if (!$main.length || !$nav.length || !$nav.hasClass('slick-initialized')) {
506            return;
507        }
508
509        $('#carouselNav').attr({ role: 'group', 'aria-label': 'Úvodná prezentácia' });
510
511        function sync() {
512            var current = $main.find('.slick-current').attr('data-slick-index');
513
514            $nav.find('.slick-slide').each(function () {
515                var $slide = $(this);
516                var real = !$slide.hasClass('slick-cloned');
517                // slick 1.8 obaluje každý náhled dalšími <div>, data-title je uvnitř
518                var title = $slide.attr('data-title') || $slide.find('[data-title]').first().attr('data-title') ||
519                    $.trim($main.find('.slick-slide[data-slick-index="' + $slide.attr('data-slick-index') + '"] .title').text());
520
521                if (!real) {
522                    $slide.attr({ 'aria-hidden': 'true', tabindex: '-1' }).removeAttr('role aria-label aria-current');
523                    return;
524                }
525                $slide.attr({ role: 'button', 'aria-label': title })
526                    .attr('tabindex', $slide.hasClass('slick-active') ? '0' : '-1')
527                    .attr('aria-hidden', $slide.hasClass('slick-active') ? 'false' : 'true');
528                if ($slide.attr('data-slick-index') === current) {
529                    $slide.attr('aria-current', 'true');
530                } else {
531                    $slide.removeAttr('aria-current');
532                }
533            });
534        }
535
536        $nav.on('keydown', '.slick-slide[role="button"]', function (e) {
537            if (e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32) {
538                e.preventDefault();
539                $main.slick('slickGoTo', parseInt($(this).attr('data-slick-index'), 10));
540            }
541        });
542        $main.add($nav).on('afterChange setPosition', function () {
543            setTimeout(sync, 0);
544        });
545        sync();
546    }
547
548    /*
549     * Záložky (jquery.tools tabs: detail inscenace, a v obsahu z CMS např.
550     * věrnostní karta) — KS 4.1.2, 2.4.3.
551     * Záložky neměly roli ani stav, a zaměření po otevření záložky šlo nejdřív
552     * přes ostatní záložky a až pak do obsahu. Vzor ARIA tabs: v Tabu je jen
553     * vybraná záložka, mezi záložkami se přechází šipkami (a Home/End),
554     * další Tab vede do obsahu vybrané záložky. Přepínání dál dělá plugin.
555     */
556    function initTabs() {
557        $('ul.tabs').each(function (g) {
558            var $list = $(this);
559            var $tabs = $list.find('> li > a');
560            var $panes = $list.nextAll('.panes').first().children('div');
561
562            // Panelů může být víc než záložek (paneVidea se renderuje vždy).
563            if (!$tabs.length || $panes.length < $tabs.length) {
564                return;
565            }
566
567            // Záložka -> panel: podle href="#id", jinak podle pořadí (jako plugin).
568            function paneOf(i) {
569                var href = $tabs.eq(i).attr('href') || '';
570                var $byId = href.length > 1 && href.charAt(0) === '#' ? $panes.filter(href) : $();
571                return $byId.length ? $byId : $panes.eq(i);
572            }
573
574            $list.attr('role', 'tablist').children('li').attr('role', 'presentation');
575            $tabs.each(function (i) {
576                var $pane = paneOf(i);
577                var paneId = $pane.attr('id') || ('zalozka-' + (g + 1) + '-' + (i + 1));
578                var tabId = 'tab-' + paneId;
579
580                $pane.attr({ id: paneId, role: 'tabpanel', 'aria-labelledby': tabId });
581                $(this).attr({ id: tabId, role: 'tab', 'aria-controls': paneId });
582            });
583
584            function sync() {
585                $tabs.each(function (i) {
586                    var selected = $(this).hasClass('active') || $(this).hasClass('current');
587                    var $pane = paneOf(i);
588                    var needsFocus = selected && $.trim($pane.text()) !== '' &&
589                        !$pane.find('a[href], button, input, select, textarea, [tabindex="0"]').length;
590
591                    $(this).attr({ 'aria-selected': selected ? 'true' : 'false', tabindex: selected ? '0' : '-1' });
592                    // Vybraný panel jen s textem musí jít zaměřit sám, aby na něj Tab
593                    // ze záložky dosáhl. Prázdný nebo nevybraný panel ne.
594                    if (needsFocus) {
595                        $pane.attr('tabindex', '0');
596                    } else {
597                        $pane.removeAttr('tabindex');
598                    }
599                });
600            }
601
602            $list.on('keydown', '[role="tab"]', function (e) {
603                var i = $tabs.index(this);
604                var next = null;
605
606                if (e.key === 'ArrowRight' || e.keyCode === 39) {
607                    next = (i + 1) % $tabs.length;
608                } else if (e.key === 'ArrowLeft' || e.keyCode === 37) {
609                    next = (i - 1 + $tabs.length) % $tabs.length;
610                } else if (e.key === 'Home' || e.keyCode === 36) {
611                    next = 0;
612                } else if (e.key === 'End' || e.keyCode === 35) {
613                    next = $tabs.length - 1;
614                } else if (e.key === ' ' || e.keyCode === 32) {
615                    e.preventDefault();
616                    $(this).trigger('click');
617                    return;
618                }
619                if (next !== null) {
620                    e.preventDefault();
621                    $tabs.eq(next).trigger('click').focus();
622                }
623            });
624
625            if (window.MutationObserver) {
626                new MutationObserver(sync).observe(this, { attributes: true, attributeFilter: ['class'], subtree: true });
627            }
628            sync();
629        });
630    }
631
632    /*
633     * Kategorie vyhledávání (predstavenie / ľudia / ostatné) — KS 4.1.2.
634     * Aktivní kategorie byla označená jen třídou. Nejde o záložky s panely,
635     * ale o přepínač, který mění výsledky (a u #listSearchFilter odešle
636     * hledání), proto aria-current, ne role="tab".
637     */
638    function initSearchCategories() {
639        var $lists = $('#listSearchFilter .list, #searchFilter .category .list');
640
641        function sync() {
642            $lists.find('.item').each(function () {
643                var $a = $(this).children('a');
644                if ($(this).hasClass('active')) {
645                    $a.attr('aria-current', 'true');
646                } else {
647                    $a.removeAttr('aria-current');
648                }
649            });
650        }
651
652        if (!$lists.length) {
653            return;
654        }
655        // Pojmenovat viditelným nadpisem („Oblasť hľadania"), kde je.
656        $lists.each(function (i) {
657            var $title = $(this).siblings('.title').first();
658            if ($title.length) {
659                $title.attr('id', $title.attr('id') || ('kategoria-hladania-' + (i + 1)));
660                $(this).attr('aria-labelledby', $title.attr('id'));
661            } else {
662                $(this).attr('aria-label', 'Kategória vyhľadávania');
663            }
664        });
665        $lists.on('click', 'a', function () {
666            setTimeout(sync, 0);
667        });
668        sync();
669    }
670
671    /*
672     * Newsletter — KS 3.3.3 (návrh opravy chyby), 3.3.1.
673     * Prohlížeč hlásil chybu jen bublinou, která zmizí, a server obecnou
674     * hláškou bez návodu. Při neplatné adrese se odeslání zastaví a pod polem
675     * zůstane text s návodem a vzorem tvaru, propojený přes aria-describedby.
676     */
677    function initNewsletter() {
678        var $form = $('#newsletter');
679        var $input = $('#newsletter-email');
680        var $error = $('#newsletter-email-error');
681        var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@.]{2,}$/;
682
683        if (!$form.length || !$input.length || !$error.length) {
684            return;
685        }
686        $form.attr('novalidate', 'novalidate');
687
688        function message(value) {
689            if ($.trim(value) === '') {
690                return 'Zadajte e-mailovú adresu, napríklad [email protected].';
691            }
692            if ((value.match(/@/g) || []).length !== 1) {
693                return 'E-mailová adresa musí obsahovaÅ¥ práve jeden znak @, napríklad [email protected].';
694            }
695            if (!EMAIL.test($.trim(value))) {
696                return 'Za znakom @ musí byÅ¥ názov domény s bodkou, napríklad [email protected].';
697            }
698            return '';
699        }
700
701        function show(text) {
702            if (text) {
703                $error.text(text).removeAttr('hidden');
704                $input.attr({ 'aria-invalid': 'true', 'aria-describedby': 'newsletter-email-error' });
705            } else {
706                $error.text('').attr('hidden', 'hidden');
707                $input.removeAttr('aria-invalid aria-describedby');
708            }
709        }
710
711        $form.on('submit', function (e) {
712            var text = message($input.val());
713            show(text);
714            if (text) {
715                e.preventDefault();
716                $input.focus();
717            }
718        });
719        // Po opravě chybu hned schovat.
720        $input.on('input', function () {
721            if ($input.attr('aria-invalid') === 'true' && !message($input.val())) {
722                show('');
723            }
724        });
725    }
726
727    /*
728     * Fotogalerie (fancybox 2) — KS 3.1.2, 4.1.2.
729     * Ovládací prvky měly anglické titulky „Previous", „Next", „Close" na
730     * slovenském webu; čtečka je četla slovenskou výslovností. Výchozí šablony
731     * se přepíší před otevřením galerie — fancybox je čte až při otevření.
732     */
733    function initLightbox() {
734        var tpl = $.fancybox && $.fancybox.defaults && $.fancybox.defaults.tpl;
735
736        if (!tpl) {
737            return;
738        }
739        tpl.closeBtn = '<a title="Zatvoriť" aria-label="Zatvoriť fotografiu" role="button" class="fancybox-item fancybox-close" href="javascript:;"></a>';
740        tpl.next = '<a title="Nasledujúca" aria-label="Nasledujúca fotografia" role="button" class="fancybox-nav fancybox-next" href="javascript:;"><span></span></a>';
741        tpl.prev = '<a title="Predchádzajúca" aria-label="Predchádzajúca fotografia" role="button" class="fancybox-nav fancybox-prev" href="javascript:;"><span></span></a>';
742        tpl.error = '<p class="fancybox-error">Obsah sa nepodarilo načítať. Skúste to, prosím, neskôr.</p>';
743    }
744
745    $(function () {
746        initMenu();
747        initLightbox();
748        initNewsletter();
749        initNavToggle();
750        initHeaderFocus();
751        initHeaderOffset();
752        initProgramMonths();
753        initProgramFilter();
754        initCarousel();
755        initTabs();
756        initSearchCategories();
757    });
758})(jQuery);

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.