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.