PageSourceSearch

https://cdn.ticketportal.cz/Scripts/tp-navigation.js?etag=1D5528AE94FD809E8074497B1F774DF1

js ticketportal.cz collected 2026-09-24 09:30:51 UTC 42,690 bytes, 1,100 lines download raw bytes

1/**
2 * TicketPortal Navigation 2026 PLG
3 * CategoryBar + SubcategoryBar interaktivita
4 */
5(function () {
6    'use strict';
7    /* ============================================
8        Configuration
9        ============================================ */
10    // true  = výbery podkategórií pretrvávajú naprieč kategóriami (multi-category)
11    // false = pri prepnutí na inú kategóriu sa predchádzajúce výbery vymažú (single-category)
12    var MULTI_CATEGORY_SELECT = false;
13
14    /* ============================================
15       Dáta: Kategórie + Podkategórie
16       ============================================ */
17
18    var CATEGORIES = tp_categories;
19
20    /*var CATEGORIES = [
21      {
22        id: 'koncerty',
23        label: 'Koncerty',
24        subcategories: [
25          'Alternativní', 'Country', 'Elektronika', 'Filmová hudba', 'Folk',
26          'Hip-Hop', 'Jazz/Blues/Swing', 'Klasická', 'Latino', 'Lidová hudba',
27          'Metal', 'Pop', 'Punk', 'R&B/Soul', 'Rap', 'Reggae', 'Rock',
28          'Šanson', 'Hudba pro děti'
29        ]
30      },
31      {
32        id: 'divadlo',
33        label: 'Divadlo',
34        subcategories: [
35          'Alternativní', 'Balet', 'Černé divadlo', 'Činohra', 'Drama',
36          'Fyzické divadlo', 'Improvizace', 'Interaktivní', 'Kabaret', 'Komedie',
37          'Kouzla', 'Loutkové', 'Muzikál', 'Nový cirkus', 'Opera', 'Performance',
38          'Pohádka', 'Představení pro děti', 'Scénické čtení', 'Site-specific',
39          'Slam poetry', 'Storytelling', 'Tanec', 'Tragédie', 'Tragikomedie',
40          'Workshop', 'Burleska', 'Cirkus', 'Stand-up comedy', 'Drag',
41          'Talk shows & podcasty', 'Laterna Magika'
42        ]
43      },
44      {
45        id: 'vystavy',
46        label: 'Výstavy',
47        subcategories: [
48          'Architektura a urbanismus', 'Design', 'Fotografie', 'Grafika',
49          'Historické', 'Instalace', 'Interaktivní', 'Klasické',
50          'Kombinovaná technika', 'Kresba', 'Malba', 'Móda', 'Nová média',
51          'Objekty', 'Performance', 'Sklo', 'Současné', 'Věda a technika',
52          'Vernisáž'
53        ]
54      },
55      {
56        id: 'festivaly',
57        label: 'Festivaly',
58        subcategories: [
59          'Divadelní festivaly', 'Festivaly pro děti', 'Festivaly umění a designu',
60          'Filmové festivaly', 'Gastro festivaly', 'Hudební festivaly',
61          'Lifestyle festivaly', 'Multižánrové festivaly', 'Sportovní festivaly'
62        ]
63      },
64      {
65        id: 'parties',
66        label: 'Parties',
67        subcategories: [
68          'Disco', 'Drum and bass', 'Dubstep', 'EDM', 'Elektronika',
69          'Hardstyle', 'House', 'Latinskoamerická hudba', 'Oldies',
70          'Psychedelic', 'Dance', 'Techno', 'Trance'
71        ]
72      },
73      {
74        id: 'filmy',
75        label: 'Filmy',
76        subcategories: [
77          'Akční', 'Animovaný', 'Biografie', 'Dobrodružný', 'Dokument',
78          'Drama', 'Erotický', 'Experimentální', 'Fantasy', 'Historický',
79          'Horor', 'Katastrofický', 'Komedie', 'Krátkometrážní', 'Krimi',
80          'Muzikál', 'Pohádka', 'Film pro děti', 'Psychologický', 'Rodinný',
81          'Romantický', 'Sci-fi', 'Sportovní', 'Studentský', 'Thriller',
82          'Válečný', 'Western', 'Záznam'
83        ]
84      },
85      {
86        id: 'gastro',
87        label: 'Gastro',
88        subcategories: [
89          'Brunch', 'Degustace', 'Kulinářské kurzy', 'Oběd', 'Snídaně',
90          'Trhy', 'Večeře', 'Vege/vegan', 'Alkoholické nápoje'
91        ]
92      },
93      {
94        id: 'pro-deti',
95        label: 'Pro děti',
96        subcategories: [
97          'Pro nejmladší', 'Vzdělávací', 'Workshop pro děti',
98          'Zábavné akce', 'Pohybové aktivity pro děti'
99        ]
100      },
101      {
102        id: 'sport',
103        label: 'Sport',
104        subcategories: [
105          'Běh', 'Bojové sporty', 'Esports', 'Extrémní sporty', 'Jóga',
106          'Motorsport', 'Fotbal', 'Hokej', 'Tenis', 'Basketball', 'Outdoor',
107          'Atletika', 'Zimní sporty', 'Koňské sporty', 'Rugby', 'Volejbal',
108          'Florbal', 'Házená', 'Vodní aktivity', 'Šipky', 'Baseball',
109          'Kulturistika', 'Míčové sporty'
110        ]
111      },
112      {
113        id: 'o2-arena',
114        label: 'O₂ Arena',
115        isO2: true,
116        subcategories: []
117      },
118      {
119        id: 'jine-akce',
120        label: 'Jiné akce',
121        subcategories: [
122          'Architektura a urbanismus', 'Cestování', 'Diskuse', 'Eco', 'Hra',
123          'Komentovaná prohlídka', 'Konference', 'Kurz', 'Kvíz', 'Lifestyle',
124          'Literatura', 'Meeting', 'Móda', 'Oslava', 'Ples', 'Přednáška',
125          'Projekce', 'Show/Revue', 'Slam poetry', 'Trh', 'Umění a design',
126          'Veletrh', 'Venkovní akce', 'Věda a technologie', 'Vycházka',
127          'Vzdělávání', 'Workshop', 'Zábava a zážitky', 'Zvířata a mazlíčci'
128        ]
129      }
130    ];*/
131    
132    /* ============================================
133       Stav
134       ============================================ */
135    var state = {
136        activeCategory: null,   // id kategórie alebo null
137        selectedSubs: {},       // { categoryId: Set<názovPodkategórie> }
138        dropdownOpen: false,
139        mobileSheetOpen: false,
140        sheetClosing: false,        // panel sa prave zasuva
141        pendingStateChange: false   // caka sa na prestavbu obsahu, viz applyFilters()
142    };
143
144    /* Sledovanie pulzovania badge */
145    var previousBadgeCounts = {};
146
147    /* ============================================
148       DOM Referencie (kešované po inicializácii)
149       ============================================ */
150    var dom = {};
151
152    /* ============================================
153       Pomocné funkcie
154       ============================================ */
155    function getCategoryById(id) {
156        for (var i = 0; i < CATEGORIES.length; i++) {
157            if (CATEGORIES[i].id === id) return CATEGORIES[i];
158        }
159        for (var i = 0; i < CATEGORIES.length; i++) {
160            if (slugify(CATEGORIES[i].id) === id) return CATEGORIES[i];
161        }
162        return null;
163    }
164
165    function getSelectedCount(categoryId) {
166        var set = state.selectedSubs[categoryId];
167        return set ? set.size : 0;
168    }
169
170    function isMobile() {
171        return window.innerWidth < 768;
172    }
173     
174    function slugify(text) {
175        return text.toLowerCase()
176            .replace(/[áàâä]/g, 'a').replace(/[éèêë]/g, 'e').replace('ě', 'e').replace('ř', 'r')
177            .replace(/[íìîï]/g, 'i').replace(/[óòôö]/g, 'o')
178            .replace(/[úùûü]/g, 'u').replace(/[ýÿ]/g, 'y')
179            .replace(/[čć]/g, 'c').replace(/[šś]/g, 's').replace(/[žź]/g, 'z')
180            .replace(/[řŕ]/g, 'r').replace(/[ňń]/g, 'n').replace(/ť/g, 't')
181            .replace(/ď/g, 'd').replace(/[ľĺ]/g, 'l')
182            .replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
183    }
184
185    /* ============================================
186       URL Smerovanie
187       ============================================ */
188    function updateURL() {
189        var newPath = '/';
190        var newSearch = '';
191
192        if (state.activeCategory) {
193            newPath = '/' + slugify(state.activeCategory);
194            var count = getSelectedCount(state.activeCategory);
195            if (count === 1) {
196                var sub = Array.from(state.selectedSubs[state.activeCategory])[0];
197                newPath += '/' + slugify(sub);
198            } else if (count > 1) {
199                var subs = Array.from(state.selectedSubs[state.activeCategory]);
200                newSearch = '?sub=' + subs.map(slugify).join(',');
201            }
202        }
203
204        window.history.replaceState(null, '', newPath + newSearch);
205    }
206
207    function readURL() {
208        var pathname = window.location.pathname;
209        // Rozdelenie cesty, filtrovanie prázdnych segmentov a nÃ
209¡zvov súborov (napr. index.html)
210        var parts = pathname.replace(/^\//, '').split('/').filter(function (p) {
211            return p.length > 0 && p.indexOf('.') === -1;
212        });
213
214        if (parts.length === 0) return;
215
216        var kat = parts[0];
217        var cat = getCategoryById(kat);
218        if (!cat) return;
219
220        state.activeCategory = kat;
221
222        // 1 podkategória → segment cesty
223        if (parts[1] && cat.subcategories.length > 0) {
224            var slug = parts[1];
225            state.selectedSubs[kat] = new Set();
226            cat.subcategories.forEach(function (sub) {
227                if (slugify(sub) === slug) {
228                    state.selectedSubs[kat].add(sub);
229                }
230            });
231            OnStateChange();
232            return;
233        }
234
235        // 2+ podkategórie → query parameter ?sub=...
236        var params = new URLSearchParams(window.location.search);
237        var subParam = params.get('sub');
238        if (subParam && cat.subcategories.length > 0) {
239            var slugs = subParam.split(',');
240            state.selectedSubs[kat] = new Set();
241            cat.subcategories.forEach(function (sub) {
242                if (slugs.indexOf(slugify(sub)) !== -1) {
243                    state.selectedSubs[kat].add(sub);
244                }
245            });
246        }
247        OnStateChange();
248    }
249
250    /* ============================================
251       SVG Ikony (inline)
252       ============================================ */
253    var SVG_CHEVRON = '<svg class="tp-category-bar__chevron" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L5 5L9 1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
254
255    var SVG_CHECK = '<svg class="tp-subcategory-pill__check" viewB
255ox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 7L6 10L11 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
256
257    var SVG_CLOSE = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 3L11 11M11 3L3 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
258
259    var SVG_CHEVRON_UP = '<svg viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:10px;height:6px"><path d="M9 5L5 1L1 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
260
261    /* ============================================
262       Renderovanie: CategoryBar
263       ============================================ */
264    function renderCategoryBar() {
265        var list = dom.categoryList;
266
267        // Odstrániť iba položky kategórií, ponechať indikátor
268        var items = list.querySelectorAll('.tp-category-bar__item');
269        items.forEach(function (item) { item.remove(); });
270
271        CATEGORIES.forEach(function (cat) {
272            var li = document.createElement('li');
273            li.setAttribute('role', 'tab');
274            li.setAttribute('tabindex', '0');
275            li.setAttribute('aria-selected', state.activeCategory === cat.id ? 'true' : 'false');
276            li.setAttribute('data-category', cat.id);
277            li.className = 'tp-category-bar__item' +
278                (state.activeCategory === cat.id ? ' tp-category-bar__item--active' : '');
279
280            var labelHTML = '';
281            if (cat.isO2) {
282                labelHTML = '<span class="tp-category-bar__o2">O₂</span> Arena';
283            } else {
284                labelHTML = cat.label;
285            }
286
287            var count = getSelectedCount(cat.id);
288            var prevCount = previousBadgeCounts[cat.id] || 0;
289            // Kym je otvoreny spodny panel, badge za nim nepulzuje - vyber
290            // pilulky nema rozhybat nic mimo samotnej pilulky.
291            var shouldPulse = count > 0 && count !== prevCount && !state.mobileSheetOpen;
292            previousBadgeCounts[cat.id] = count;
293
294            var badgeHTML = '<span class="tp-category-bar__badge' +
295                (count > 0 ? ' tp-category-bar__badge--visible' : '') +
296                (shouldPulse ? ' tp-category-bar__badge--pulse' : '') +
297                '">' + count + '</span>';
298
299            var chevronHTML = cat.subcategories.length > 0 && state.activeCategory === cat.id
300                ? SVG_CHEVRON : '';
301
302            li.innerHTML = '<span>' + labelHTML + '</span>' + badgeHTML + chevronHTML;
303            // Vložiť pred indikátor aby zostal ako posledný potomok
304            list.insertBefore(li, dom.indicator);
305
306            if (shouldPulse) {
307                var badge = li.querySelector('.tp-category-bar__badge');
308                if (badge) {
309                    badge.addEventListener('animationend', function handler() {
310                        badge.classList.remove('tp-category-bar__badge--pulse');
311                        badge.removeEventListener('animationend', handler);
312                    });
313                }
314            }
315        });
316
317        requestAnimationFrame(function () {
318            updateIndicator();
319        });
320    }
321
322    /* ============================================
323       Posuvný aktívny indikátor
324       ============================================ */
325    function updateIndicator() {
326        var activeItem = dom.categoryList.querySelector('.tp-category-bar__item--active');
327        if (!activeItem || !dom.indicator) {
328            if (dom.indicator) dom.indicator.classList.remove('tp-category-bar__indicator--visible');
329            return;
330        }
331
332        var listRect = dom.categoryList.getBoundingClientRect();
333        var itemRect = activeItem.getBoundingClientRect();
334
335        dom.indicator.style.left = (itemRect.left - listRect.left) + 'px';
336        dom.indicator.style.width = itemRect.width + 'px';
337        dom.indicator.classList.add('tp-category-bar__indicator--visible');
338    }
339
340    /* ============================================
341       Renderovanie: SubcategoryDropdown (Desktop)
342       ============================================ */
343    function renderDropdown(isInitialOpen) {
344        var dropdown = dom.dropdown;
345        if (!state.activeCategory || !state.dropdownOpen || isMobile()) {
346            dropdown.classList.remove('tp-subcategory-dropdown--open');
347            dropdown.classList.remove('tp-subcategory-dropdown--animating');
348            return;
349        }
350
351        var cat = getCategoryById(state.activeCategory);
352        if (!cat || cat.subcategories.length === 0) {
353            dropdown.classList.remove('tp-subcategory-dropdown--open');
354            return;
355        }
356
357        var selected = state.selectedSubs[state.activeCategory] || new Set();
358        var count = selected.size;
359
360        // Hlavička
361        var headerHTML = '<div class="tp-subcategory-dropdown__header">' +
362            '<div class="tp-subcategory-dropdown__breadcrumb">' +
363            '<span class="tp-subcategory-dropdown__breadcrumb-category">' + cat.label + '</span>' +
364            (count > 0 ? '<span class="tp-subcategory-dropdown__breadcrumb-count">' + count + ' ' + lang["lbVybranych"] + '</span>' : '') +
365            '</div>' +
366            '<button class="tp-subcategory-dropdown__close-link' +
367            (count === 0 ? ' tp-subcategory-dropdown__close-link--visible' : '') +
368            '" data-action="close-dropdown">' + SVG_CHEVRON_UP + ' ' + lang["lbZavriet"] + '</button>' +
369            '<button class="tp-subcategory-dropdown__cancel' +
370            (count > 0 ? ' tp-subcategory-dropdown__cancel--visible' : '') +
371            '" data-action="cancel-filter">
371' + SVG_CLOSE + ' ' + lang["lbZrusitFilter"] + '</button>' +
372            '</div>';
373
374        // Pilulky (pills)
375        var pillsHTML = '<ul class="tp-subcategory-dropdown__pills" role="listbox" aria-multiselectable="true">';
376
377        // Pilulka "Všetko"
378        var allActive = count === 0;
379        pillsHTML += '<li><button class="tp-subcategory-pill tp-subcategory-pill--all' +
380            (allActive ? ' tp-subcategory-pill--active' : '') +
381            '" role="option" aria-selected="' + allActive + '" data-action="select-all" style="--pill-index: 0">' +
382            SVG_CHECK + ' ' + lang["lbVsetko"] + '</button></li>';
383
384        // Pilulky podkategórií
385        var pillIndex = 1;
386        cat.subcategories.forEach(function (sub) {
387            var isActive = selected.has(sub);
388            pillsHTML += '<li><button class="tp-subcategory-pill' +
389                (isActive ? ' tp-subcategory-pill--active' : '') +
390                '" role="option" aria-selected="' + isActive + '" data-subcategory="' + sub + '" style="--pill-index: ' + pillIndex + '">' +
391                SVG_CHECK + ' ' + sub + '</button></li>';
392            pillIndex++;
393        });
394
395        pillsHTML += '</ul>';
396
397        dom.dropdownContainer.innerHTML = headerHTML + pillsHTML;
398        dropdown.classList.add('tp-subcategory-dropdown--open');
399
400        if (isInitialOpen) {
401            dropdown.classList.add('tp-subcategory-dropdown--animating');
402            var pillCount = cat.subcategories.length + 1;
403            var totalDuration = 300 + (pillCount * 20);
404            setTimeout(function () {
405                dropdown.classList.remove('tp-subcategory-dropdown--animating');
406            }, totalDuration);
407        }
408    }
409
410    /* ============================================
411       Renderovanie: Mobilný Bottom Sheet
412       ============================================ */
413    function renderMobileSheet() {
414        if (!state.activeCategory) return;
415
416        var cat = getCategoryById(state.activeCategory);
417        if (!cat || cat.subcategories.length === 0) return;
418
419        var selected = state.selectedSubs[state.activeCategory] || new Set();
420        var count = selected.size;
421
422        // Nadpis
423        dom.mobileSheetTitle.innerHTML = cat.label +
424            (count > 0 ? ' <span class="tp-mobile-sheet__title-badge">' + count + '</span>' : '');
425
426        // Ak pills už existujú, iba prepni --active triedy (bez re-animácie)
427        var existingPills = dom.mobileSheetPills.querySelectorAll('.tp-subcategory-pill');
428        if (existingPills.length > 0) {
429            var allActive = count === 0;
430            existingPills.forEach(function (pill) {
431                if (pill.getAttribute('data-action') === 'select-all') {
432                    pill.classList.toggle('tp-subcategory-pill--active', allActive);
433                } else {
434                    var sub = pill.getAttribute('data-subcategory');
435                    pill.classList.toggle('tp-subcategory-pill--active', sub ? selected.has(sub) : false);
436                }
437            });
438        } else {
439            // Prvé renderovanie — vytvoriť pills HTML
440            var html = '';
441            var allActive = count === 0;
442            var pillIndex = 0;
443            html += '<li><button class="tp-subcategory-pill tp-subcategory-pill--all' +
444                (allActive ? ' tp-subcategory-pill--active' : '') +
445                '" data-action="select-all" style="--pill-index: ' + pillIndex + '">' + SVG_CHECK + ' ' + lang["lbVsetko"] + '</button></li>';  
446            pillIndex++;
447
448            cat.subcategories.forEach(function (sub) {
449                var isActive = selected.has(sub);
450                html += '<li><button class="tp-subcategory-pill' +
451                    (isActive ? ' tp-subcategory-pill--active' : '') +
452                    '" data-subcategory="' + sub + '" style="--pill-index: ' + pillIndex + '">' + SVG_CHECK + ' ' + sub + '</button></li>';
453                pillIndex++;
454            });
455
456            dom.mobileSheetPills.innerHTML = html;
457        }
458
459        // Tlačidlo odoslania
460        dom.mobileSheetSubmit.textContent = count > 0
461            ? lang["lbZobrazitVybranych"] + ' ' + count
462            : lang["lbZobrazitVsetky"];
463    }
464
465    /* ============================================
466       Renderovanie: Mobilný Filter Bar (čipy)
467       ============================================ */
468    function renderMobileFilterBar() {
469        if (!state.activeCategory || (!isMobile() && state.dropdownOpen)) {
470            dom.mobileFilterBar.classList.remove('tp-mobile-filter-bar--visible');
471            return;
472        }
473
474        var selected = state.selectedSubs[state.activeCategory];
475        if (!selected || selected.size === 0) {
476            dom.mobileFilterBar.classList.remove('tp-mobile-filter-bar--visible');
477            return;
478        }
479
480        var html = '';
481        selected.forEach(function (sub) {
482            html += '<button class="tp-mobile-filter-chip" data-remove-sub="' + sub + '">' +
483                sub + ' <span class="tp-mobile-filter-chip__remove">×</span></button>';
484        });
485
486        dom.mobileFilterChips.innerHTML = html;
487        dom.mobileFilterBar.classList.add('tp-mobile-filter-bar--visible');
488    }
489
490    /* ============================================
491       Akcie
492       ============================================ */
493    function openCategory(categoryId) {
494        var cat = getCategoryById(categoryId);
495        if (!cat) return;
496
497        // Ak sa klikne na rovnakú kategóriu bez filtrov → zavrieť
498        if (state.activeCategory === categoryId && getSelectedCount(categoryId) === 0) {
499            closeDropdown();
500            return;
501        }
502
503        // Single-category režim: vymazať výbery vo všetkých ostatných kategóriách
504        if (!MULTI_CATEGORY_SELECT && state.activeCategory !== categoryId) {
505            Object.keys(state.selectedSubs).forEach(function (key) {
506                if (key !== categoryId) delete state.selectedSubs[key];
507            });
508        }
509
510        state.activeCategory = categoryId;
511        state.dropdownOpen = true;
512
513        renderCategoryBar();
514        updateURL();
515
516        if (isMobile()) {
517            if (cat.subcategories.length > 0) {
518                openMobileSheet();
519                // OnStateChange() rozbehne Generate_timeline(): prestavia cely
520                // zoznam podujati, slider, horizontalnu casovu os, Top/Novinky/
521                // Zlavy - desiatky az stovky milisekund suvislej prace na
522                // hlavnom vlakne. Kym bezalo hned po otvoreni panela, panel na
523                // ten cas zamrzol v polovici vysunutia a dobehol az ked prace
524                // skoncila. Preto to bolo pri kazdej kategorii inak dlhe -
525                // kolko obsahu sa prestavuje, tolko trvalo zamrznutie.
526                // Spustame to az ked je panel vonku; pouzivatel sa vtedy pozera
527                // na panel a obsah je aj tak pod rozmazanym prekrytim.
528                afterSheetOpen(OnStateChange);
529                return;
530            }
531        } else {
532            renderDropdown(true);
533            renderMobileFilterBar();
534        }
535        applyFilters();
536    }
537
538    // Spusti fn az ked dobehne vysunutie spodneho panela. Poistka na 600 ms
539    // pokryva pripad, ked sa transitionend nedostavi (prerusena animacia,
540    // prepnuty tab, prefers-reduced-motion).
541    function afterSheetOpen(fn) {
542        var sheet = dom.mobileSheet;
543        var done = false;
544
545        function run() {
546            if (done) return;
547            done = true;
548            sheet.removeEventListener('transitionend', onEnd);
549            fn();
550        }
551
552        function onEnd(e) {
553            if (e.target === sheet && e.propertyName === 'transform') run();
554        }
555
556        sheet.addEventListener('transitionend', onEnd);
557        setTimeout(run, 600);
558    }
559
560    function closeDropdown() {
561        state.activeCategory = null;
562        state.dropdownOpen = false;
563
564        renderCategoryBar();
565        renderDropdown();
566        renderMobileFilterBar();
567        updateURL();
568        applyFilters();
569    }
570
571    function toggleSubcategory(categoryId, subName) {
572        if (!state.selectedSubs[categoryId]) {
573            state.selectedSubs[categoryId] = new Set();
574        }
575
576        var set = state.selectedSubs[categoryId];
577        if (set.has(subName)) {
578            set.delete(subName);
579        } else {
580            set.add(subName);
581        }
582
583        // Vyčistenie prázdnych setov
584        if (set.size === 0) {
585            delete state.selectedSubs[categoryId];
586        }
587
588        renderCategoryBar();
589        renderMobileSheet();
590        updateURL();
591
592        // Kym je otvoreny spodny panel, obsah za nim nechavame tak - je aj tak
593        // pod rozmazanym prekrytim. Prestavi sa naraz az po zatvoreni panela.
594        if (state.mobileSheetOpen) {
595            applyFilters();
596            return;
597        }
598
599        renderDropdown();
600        renderMobileFilterBar();
601        applyFilters();
602    }
603
604    function resetCategory(categoryId, scrollToTop) {
605        delete state.selectedSubs[categoryId];
606        state.activeCategory = null;
607        state.dropdownOpen = false;
608
609        renderCategoryBar();
610        renderDropdown();
611        renderMobileFilterBar();
612        updateURL();
613        applyFilters();
614
615        if (scrollToTop) {
616            window.scrollTo({ top: 0, behavior: 'smooth' });
617        }
618    }
619
620    function openMobileSheet() {
621        renderMobileSheet();
622        dom.mobileOverlay.classList.add('tp-mobile-sheet-overlay--visible');
623        dom.mobileOverlay.setAttribute('aria-hidden', 'false');
624        state.mobileSheetOpen = true;
625        document.body.style.overflow = 'hidden';
626
627        // Dva ramce, nie jeden. V prvom ramci sa este len vykresli vsetko, co
628        // sme prave zmenili - pilulky, rozmazane prekrytie a hlavne zamknutie
629        // rolovania stranky (to preskladava cely dlhy zoznam podujati). Keby
630        // sa posun panela spustil uz v tomto ramci, jeho cas by zacal bezat
631        // pred tym drahym prekreslenim: panel by sa zjavil niekde v polovici
632        // cesty, chvilu stal a potom doskocil. Az druhy ramec ho rozbehne nad
633        // uz hotovym obrazom.
634        requestAnimationFrame(function () {
635            requestAnimationFrame(function () {
636                dom.mobileSheet.classList.add('tp-mobile-sheet--open');
637            });
638        });
639    }
640
641    // Prestavba obsahu (OnStateChange -> Generate_timeline) zamrazi stranku na
642    // desiatky az stovky milisekund. Kym je spodny panel na obrazovke alebo sa
643    // prave zasuva, odkladame ju - inak zasekne jeho animaciu. Sputi ju
644    // closeMobileSheet() v okamihu, ked je panel uz dole.
645    function applyFilters() {
646        if (state.mobileSheetOpen || state.sheetClosing) {
647            state.pendingStateChange = true;
648            return;
649        }
650        OnStateChange();
651    }
652
653    function closeMobileSheet() {
654        dom.mobileSheet.classList.remove('tp-mobile-sheet--open');
655        dom.mobileSheetPills.innerHTML = '';
656        state.mobileSheetOpen = false;
657        state.sheetClosing = true;
658        document.body.style.overflow = '';
659
660        setTimeout(function () {
661            state.sheetClosing = false;
662
663            // Poradie je zamerne: obsah sa prestavi este pod nepriehladnym
664            // prekrytim (vymena je tak neviditelna) a az potom prekrytie
665            // zhasne - odkryje uz hotovy obsah a jeho prelinanie nema co
666            // zasekavat.
667            if (state.pendingStateChange) {
668                state.pendingStateChange = false;
669                OnStateChange();
670            }
671
672            requestAnimationFrame(function () {
673                dom.mobileOverlay.classList.remove('tp-mobile-sheet-overlay--visible');
674                dom.mobileOverlay.setAttribute('aria-hidden', 'true');
675            });
676        }, 400);
677
678        renderMobileFilterBar();
679    }
680
681    /* ============================================
682       Obsluha udalostí
683       ============================================ */
684    function onCategoryClick(e) {
685        var item = e.target.closest('.tp-category-bar__item');
686        if (!item) return;
687        e.stopPropagation();
688        var categoryId = item.getAttribute('data-category');
689        openCategory(categoryId);
690    }
691
692    function onCategoryKeydown(e) {
693        var item = e.target.closest('.tp-category-bar__item');
694        if (!item) return;
695
696        if (e.key === 'Enter' || e.key === ' ') {
697            e.preventDefault();
698            var categoryId = item.getAttribute('data-category');
699            openCategory(categoryId);
700            return;
701        }
702
703        var items = dom.categoryList.querySelectorAll('.tp-category-bar__item');
704        var index = Array.prototype.indexOf.call(items, item);
705
706        if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
707            e.preventDefault();
708            var next = items[(index + 1) % items.length];
709            next.focus();
710        } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
711            e.preventDefault();
712            var prev = items[(index - 1 + items.length) % items.length];
713            prev.focus();
714        }
715    }
716
717    function onDropdownClick(e) {
718        var collapse = e.target.closest('.tp-subcategory-dropdown__collapse');
719        if (collapse) {
720            e.stopPropagation();
721            // Zavrieť dropdown bez resetovania filtrov
722            if (getSelectedCount(state.activeCategory) > 0) {
723                state.dropdownOpen = false;
724                dom.dropdown.classList.remove('tp-subcategory-dropdown--open');
725                renderMobileFilterBar();
726            } else {
727                closeDropdown();
728            }
729            return;
730        }
731
732        var pill = e.target.closest('.tp-subcategory-pill');
733        if (pill) {
734            e.stopPropagation();
735            if (pill.getAttribute('data-action') === 'select-all') {
736                resetCategory(state.activeCategory, true);
737            } else {
738                var sub = pill.getAttribute('data-subcategory');
739                if (sub) {
740                    toggleSubcategory(state.activeCategory, sub);
741                }
742            }
743            return;
744        }
745
746        var closeLink = e.target.closest('[data-action="close-dropdown"]');
747        if (closeLink) {
748            e.stopPropagation();
749            closeDropdown();
750            return;
751        }
752
753        var cancel = e.target.closest('[data-action="cancel-filter"]');
754        if (cancel) {
755            e.stopPropagation();
756            resetCategory(state.activeCategory, false);
757        }
758    }
759
760    function onMobileSheetClick(e) {
761        var pill = e.target.closest('.tp-subcategory-pill');
762        if (pill) {
763            if (pill.getAttribute('data-action') === 'select-all') {
764                var catId = state.activeCategory;
765                closeMobileSheet();
766                resetCategory(catId, true);
767            } else {
768                var sub = pill.getAttribute('data-subcategory');
769                if (sub) {
770                    toggleSubcategory(state.activeCategory, sub);
771                }
772            }
773            return;
774        }
775
776        var reset = e.target.closest('.tp-mobile-sheet__reset');
777        if (reset) {
778            var catId = state.activeCategory;
779            closeMobileSheet();
780            resetCategory(catId, true);
781            return;
782        }
783
784        var close = e.target.closest('.tp-mobile-sheet__close');
785        if (close) {
786            closeMobileSheet();
787            return;
788        }
789
790        var submit = e.target.closest('.tp-mobile-sheet__submit');
791        if (submit) {
792            closeMobileSheet();
793        }
794    }
795
796    function onMobileFilterChipClick(e) {
797        var cancelBtn = e.target.closest('.tp-mobile-filter-bar__cancel');
798        if (cancelBtn) {
799            e.stopPropagation();
800            resetCategory(state.activeCategory, false);
801            return;
802        }
803
804        var chip = e.target.closest('.tp-mobile-filter-chip');
805        if (!chip) return;
806
807        var sub = chip.getAttribute('data-remove-sub');
808        if (sub && state.activeCategory) {
809            var catId = state.activeCategory;
810            var selected = state.selectedSubs[catId];
811
812            // Animovať odstránenie čipu pred zmenou stavu
813            chip.classList.add('tp-mobile-filter-chip--removing');
814            chip.addEventListener('animationend', function handler() {
815                chip.removeEventListener('animationend', handler);
816                // Ak je toto posledný čip, urobiť plný reset (rovnako ako "Zrušiť filter" / "Všetko")
817                if (selected && selected.size === 1 && selected.has(sub)) {
818                    resetCategory(catId, true);
819                } else {
820                    toggleSubcategory(catId, sub);
821                }
822            });
823        }
824    }
825
826    /* ============================================
827       Dotyk: Swipe nadol na zatvorenie mobilného sheetu
828       ============================================ */
829    var touchState = { startY: 0, currentY: 0, swiping: false };
830
831    function onTouchStart(e) {
832        var handle = e.target.closest('.tp-mobile-sheet__drag-handle, .tp-mobile-sheet__header');
833        if (!handle) return;
834        touchState.startY = e.touches[0].clientY;
835        touchState.swiping = true;
836    }
837
838    function onTouchMove(e) {
839        if (!touchState.swiping) return;
840        touchState.currentY = e.touches[0].clientY;
841        var diff = touchState.currentY - touchState.startY;
842        if (diff > 0) {
843            dom.mobileSheet.style.transform = 'translateY(' + diff + 'px)';
844        }
845    }
846
847    function onTouchEnd() {
848        if (!touchState.swiping) return;
849        touchState.swiping = false;
850        var diff = touchState.currentY - touchState.startY;
851
852        if (diff > 100) {
853            closeMobileSheet();
854        }
855
856        dom.mobileSheet.style.transform = '';
857    }
858
859    /* ============================================
860       Inicializácia
861       ============================================ */
862    function init() {
863        // Kešovanie DOM
864        dom.categoryList = document.querySelector('.tp-category-bar__list');
865        dom.dropdown = document.querySelector('.tp-subcategory-dropdown');
866        dom.dropdownContainer = document.querySelector('.tp-subcategory-dropdown__container');
867        dom.mobileOverlay = document.querySelector('.tp-mobile-sheet-overlay');
868        dom.mobileSheet = document.querySelector('.tp-mobile-sheet');
869        dom.mobileSheetTitle = document.querySelector('.tp-mobile-sheet__title');
870        dom.mobileSheetPills = document.querySelector('.tp-mobile-sheet__pills');
871        dom.mobileSheetSubmit = document.querySelector('.tp-mobile-sheet__submit');
872        dom.mobileFilterBar = document.querySelector('.tp-mobile-filter-bar');
873        dom.mobileFilterChips = document.querySelector('.tp-mobile-filter-bar__chips');
874        dom.indicator = document.querySelector('.tp-category-bar__indicator');
875
876        // Načítanie stavu z URL
877        readURL();
878
879        // Úvodné renderovanie
880        renderCategoryBar();
881        if (state.activeCategory && !isMobile()) {
882            state.dropdownOpen = true;
883            renderDropdown(true);
884        }
885        if (state.activeCategory && isMobile()) {
886            renderMobileFilterBar();
887        }
888
889        // Poslucháči udalostí
890        dom.categoryList.addEventListener('click', onCategoryClick);
891        dom.categoryList.addEventListener('keydown', onCategoryKeydown);
892        dom.dropdown.addEventListener('click', onDropdownClick);
893        dom.mobileSheet.addEventListener('click', onMobileSheetClick);
894        dom.mobileOverlay.addEventListener('click', function (e) {
895            if (e.target === dom.mobileOverlay) closeMobileSheet();
896        });
897        dom.mobileFilterBar.addEventListener('click', onMobileFilterChipClick);
898
899        // Dotykové udalosti pre mobilný sheet
900        dom.mobileSheet.addEventListener('touchstart', onTouchStart, { passive: true });
901        dom.mobileSheet.addEventListener('touchmove', onTouchMove, { passive: true });
902        dom.mobileSheet.addEventListener('touchend', onTouchEnd, { passive: true });
903
904        // Klávesa Escape
905        document.addEventListener('keydown', function (e) {
906            if (e.key === 'Escape') {
907                if (state.mobileSheetOpen) {
908                    closeMobileSheet();
909                } else if (state.dropdownOpen) {
910                    closeDropdown();
911                }
912            }
913        });
914
915        // Responzivita: zavrieť mobilný sheet ak sa zväčší na desktop
916        window.addEventListener('resize', function () {
917            if (!isMobile() && state.mobileSheetOpen) {
918                closeMobileSheet();
919                if (state.activeCategory) renderDropdown(true);
920            }
921            updateIndicator();
922        });
923    }
924
925    function OnStateChange() {
926        if (state.activeCategory != null) {
927            if (typeof (state.selectedSubs[state.activeCategory]) == "undefined") {         // Zvolená je iba hlavná kategória
928                var id_kat = getCategoryIdFromName(state.activeCategory);
929
930                if (search_param.kategorie == null) {
931                    Pridaj_filter(null, 'kategoria', id_kat);
932                }
933                else {
934                    var obsahuje = false;
935                    for (var i in search_param.kategorie) {
936                        if (search_param.kategorie[i].id != id_kat) {
937                            if (search_param.kategorie[i].id_rodic == null)
938                                Pridaj_filter(null, 'kategoria', search_param.kategorie[i].id);
939                            else
940                                Pridaj_filter(null, 'podkategoria', search_param.kategorie[i].id);
941                        }
942                        else {
943                            obsahuje = true;
944                        }
945                    }
946                    if (!obsahuje)
947                        Pridaj_filter(null, 'kategoria', id_kat);
948                }
949            }
950            else {  // Sú zvolené vedľajšie kategórie
951                var subkat = Array.from(state.selectedSubs[state.activeCategory]);
952                var subkat_id = [];
953                for (var i = 0; i < subkat.length; i++)
954                    subkat_id[getCategoryIdFromName(subkat[i])] = false;
955
956                for (var i in search_param.kategorie) {
957                    var id = search_param.kategorie[i].id;
958                    if (subkat_id[id] == null)
959                        Pridaj_filter(null, 'podkategoria', id);
960                    else
961                        subkat_id[id] = true;
962                }
963                for (var i in subkat_id) {
964                    if (subkat_id[i] == false)
965                        Pridaj_filter(null, 'podkategoria', i);
966                }
967
968            }
969        }
970        else {      // Filter je prázdny
971            if (search_param.kategorie != null) {
972                for (var i in search_param.kategorie) {
973                    if (search_param.kategorie[i].id_rodic == null)
974                        Pridaj_filter(null, 'kategoria', search_param.kategorie[i].id);
975                    else
976                        Pridaj_filter(null, 'podkategoria', search_param.kategorie[i].id);
977                }
978            }
979            if (search_param.kategorie != null) {
980                for (var i in search_param.kategorie) {
981                    if (search_param.kategorie[i].id_rodic == null)
982                        Pridaj_filter(null, 'kategoria', search_param.kategorie[i].id);
983                    else
984                        Pridaj_filter(null, 'podkategoria', search_param.kategorie[i].id);
985                }
986            }
987        }
988    }
989
990    function getCategoryIdFromName(name) {
991        for (var i in dic_kategoria) {
992            if (slugify(dic_kategoria[i].nazov) == slugify(name))
993                return dic_kategoria[i].id;
994        }
995    }
996
997
998  // Spustenie keď je DOM pripravený
999  if (document.readyState === 'loading') {
1000    document.addEventListener('DOMContentLoaded', init);
1001  } else {
1002    init();
1003  }
1004
1005  // === MOBILNÝ SIDEBAR ===
1006  // Aktívny na mobile (< 768px), skrytý na desktope cez CSS.
1007  //initSidebar();
1008
1009  function initSidebar() {
1010    var sidebar = document.querySelector('.tp-sidebar');
1011    var overlay = document.querySelector('.tp-sidebar__overlay');
1012    if (!sidebar || !overlay) return;
1013
1014    var closeBtn = sidebar.querySelector('.tp-sidebar__close');
1015    var accordionHeaders = sidebar.querySelectorAll('.tp-sidebar__accordion-header');
1016
1017    function openSidebar() {
1018      sidebar.classList.add('tp-sidebar--open');
1019      overlay.classList.add('tp-sidebar__overlay--visible');
1020      overlay.setAttribute('aria-hidden', 'false');
1021      document.body.style.overflow = 'hidden';
1022    }
1023
1024    function closeSidebar() {
1025      sidebar.classList.remove('tp-sidebar--open');
1026      overlay.classList.remove('tp-sidebar__overlay--visible');
1027      overlay.setAttribute('aria-hidden', 'true');
1028      document.body.style.overflow = '';
1029    }
1030
1031    // Hamburger trigger — nájsť existujúce mobilné menu prepínacie tlačidlo
1032    var hamburger = document.querySelector('.toggle-push-left, [data-toggle="tp-sidebar"]');
1033    if (hamburger) {
1034      hamburger.addEventListener('click', function (e) {
1035        e.preventDefault();
1036        e.stopPropagation();
1037        openSidebar();
1038      });
1039    }
1040
1041    // Tlačidlo zatvorenia
1042    if (closeBtn) {
1043      closeBtn.addEventListener('click', closeSidebar);
1044    }
1045
1046    // Klik na overlay
1047    overlay.addEventListener('click', closeSidebar);
1048
1049    // Klávesa Escape
1050    document.addEventListener('keydown', function (e) {
1051      if (e.key === 'Escape' && sidebar.classList.contains('tp-sidebar--open')) {
1052        closeSidebar();
1053      }
1054    });
1055
1056    // Prepínanie akordeónu
1057    for (var i = 0; i < accordionHeaders.length; i++) {
1058      accordionHeaders[i].addEventListener('click', function () {
1059        var expanded = this.getAttribute('aria-expanded') === 'true';
1060        var body = this.nextElementSibling;
1061        if (expanded) {
1062          this.setAttribute('aria-expanded', 'false');
1063          body.style.maxHeight = null;
1064        } else {
1065          this.setAttribute('aria-expanded', 'true');
1066          body.style.maxHeight = body.scrollHeight + 'px';
1067        }
1068      });
1069    }
1070
1071    // Swipe vľavo na zatvorenie
1072    var touchStartX = 0;
1073    var touchCurrentX = 0;
1074    var swiping = false;
1075
1076    sidebar.addEventListener('touchstart', function (e) {
1077      touchStartX = e.touches[0].clientX;
1078      swiping = true;
1079    }, { passive: true });
1080
1081    sidebar.addEventListener('touchmove', function (e) {
1082      if (!swiping) return;
1083      touchCurrentX = e.touches[0].clientX;
1084      var diff = touchStartX - touchCurrentX;
1085      if (diff > 0) {
1086        sidebar.style.transform = 'translateX(' + (-diff) + 'px)';
1087      }
1088    }, { passive: true });
1089
1090    sidebar.addEventListener('touchend', function () {
1091      if (!swiping) return;
1092      swiping = false;
1093      var diff = touchStartX - touchCurrentX;
1094      if (diff > 80) {
1095        closeSidebar();
1096      }
1097      sidebar.style.transform = '';
1098    });
1099  }
1100})();

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.