1/** 2 * Frontend JavaScript for Tours Display Block 3 * Opravená verze s pÅesným poÄÃtánÃm a správným zobrazovánÃm pÅÃspÄvků ve filtrech 4 * PÅÃsné filtrovánà respektujÃcà vždy výchozà filtry z Gutenberg bloku 5 * S nemÄnnými, stále viditelnými a klikatelnými filtry 6 */ 7 8// Wait for DOM to be ready 9document.addEventListener('DOMContentLoaded', function () { 10 // NajÃt vÅ¡echny bloky Tours Display - obÄ verze (hlavnà blok a footer blok) 11 const toursBlocks = document.querySelectorAll('.tours-display-block, .tours-display-footer-block'); 12 const globalFilterConfig = Array.isArray(window.toursDisplayData?.filterConfig) 13 ? window.toursDisplayData.filterConfig 14 : []; 15 16 toursBlocks.forEach(function (block) { 17 // ZjiÅ¡tÄnà základnÃch informacà o bloku 18 const blockClass = block.className; 19 const isFooter = blockClass.includes('tours-display-footer-block'); 20 const displayMode = block.getAttribute('data-display-mode'); 21 const numberOfTours = parseInt(block.getAttribute('data-number-of-tours')) || 48; 22 23 // NaÄtenà vybraných ID zájezdů (pro manuálnà výbÄr) 24 const selectedTours = block.getAttribute('data-selected-tours') || ''; 25 const footerSelectedTours = block.getAttribute('data-footer-selected-tours') || ''; 26 27 // NajÃt kontejner pro karty zájezdů - buÄ standardnà nebo footer verze 28 const toursContainer = block.querySelector('.tours-display-container') || 29 block.querySelector('.tours-display-footer-container'); 30 31 if (!toursContainer) { 32 return; 33 } 34 35 const parsedBlockConfig = parseJsonAttribute(block.getAttribute('data-filter-config')); 36 const filterConfig = Array.isArray(parsedBlockConfig) && parsedBlockConfig.length 37 ? parsedBlockConfig 38 : globalFilterConfig; 39 40 if (block.classList.contains('tours-display-filter-shortcode')) { 41 initShortcodeSearchForm(block, toursContainer, filterConfig); 42 } 43 44 const parsedFilterModes = parseJsonAttribute(block.getAttribute('data-filter-modes')); 45 const filterModes = parsedFilterModes && typeof parsedFilterModes === 'object' && !Array.isArray(parsedFilterModes) 46 ? parsedFilterModes 47 : {}; 48 const travelPackageFilter = normalizeTravelPackageFilterValue(block.getAttribute('data-travel-package-filter')); 49 50 const parsedSelections = parseJsonAttribute(block.getAttribute('data-filter-selections')); 51 const originalFilters = {}; 52 const hasParsedSelections = parsedSelections && 53 typeof parsedSelections === 'object' && 54 !Array.isArray(parsedSelections) && 55 Object.keys(parsedSelections).length > 0; 56 57 if (Array.isArray(filterConfig) && filterConfig.length) { 58 filterConfig.forEach((filter) => { 59 originalFilters[filter.key] = hasParsedSelections 60 ? normalizeArrayValues(parsedSelections[filter.key]) 61 : []; 62 }); 63 } 64 65 // Legacy fallback for older block markup or stale cached HTML without data-filter-selections. 66 if (!hasParsedSelections) { 67 const defaultTypZajezdu = block.getAttribute('data-typ-zajezdu') || ''; 68 const defaultZajezdRozdeleni = block.getAttribute('data-zajezd-rozdeleni') || ''; 69 const defaultZajezdDestinace = block.getAttribute('data-zajezd-destinace') || ''; 70 71 if (defaultTypZajezdu.length) { 72 originalFilters.typ_zajezdu = normalizeArrayValues(defaultTypZajezdu); 73 } 74 if (defaultZajezdRozdeleni.length) { 75 originalFilters.zajezd_rozdeleni = normalizeArrayValues(defaultZajezdRozdeleni); 76 } 77 if (defaultZajezdDestinace.length) { 78 originalFilters.zajezd_destinace = normalizeArrayValues(defaultZajezdDestinace); 79 } 80 } 81 82 if (!Object.keys(originalFilters).length && Array.isArray(filterConfig) && filterConfig.length) { 83 filterConfig.forEach((filter) => { 84 originalFilters[filter.key] = []; 85 }); 86 } 87 88 // Uživatelské filtry (ty, které uživatel vybral) 89 const userFilters = {};
90 Object.keys(originalFilters).forEach((key) => { 91 userFilters[key] = []; 92 }); 93 94 // NajÃt filtrovacà tlaÄÃtka v tomto bloku 95 const filterButtons = block.querySelectorAll('.tours-display-filter-btn'); 96 97 const refreshButtonState = () => { 98 updateFilterButtonState(filterButtons, userFilters); 99 }; 100 refreshButtonState(); 101 102 // PÅidat event listener pro každé tlaÄÃtko 103 filterButtons.forEach(function (button) { 104 button.addEventListener('click', function (e) { 105 e.preventDefault(); 106 107 const filterKey = button.getAttribute('data-filter-key') || button.getAttribute('data-filter'); 108 const value = button.getAttribute('data-value') || ''; 109 const selectionMode = resolveSelectionMode( 110 filterConfig, 111 filterKey, 112 button.getAttribute('data-selection-mode') 113 ); 114 115 // Zobrazit loading indikátor 116 showLoading(toursContainer); 117 118 if (filterKey === 'all') { 119 for (const key in userFilters) { 120 userFilters[key] = []; 121 } 122 } else if (!filterKey) { 123 return; 124 } else if (value === '') { 125 userFilters[filterKey] = []; 126 } else if (selectionMode === 'multi') { 127 const currentValues = Array.isArray(userFilters[filterKey]) ? [...userFilters[filterKey]] : []; 128 if (currentValues.includes(value)) { 129 userFilters[filterKey] = currentValues.filter((item) => item !== value); 130 } else { 131 currentValues.push(value); 132 userFilters[filterKey] = currentValues; 133 } 134 } else { 135 userFilters[filterKey] = [value]; 136 } 137 138 refreshButtonState(); 139 140 if (isFooter && footerSelectedTours) { 141 loadFilteredTours(block, originalFilters, userFilters, numberOfTours, displayMode, selectedTours, footerSelectedTours, filterConfig, filterModes, travelPackageFilter); 142 } else { 143 loadFilteredTours(block, originalFilters, userFilters, numberOfTours, displayMode, selectedTours, null, filterConfig, filterModes, travelPackageFilter); 144 } 145 }); 146 }); 147 148 // ZajistÃme, že tlaÄÃtko "Detail zájezdu" bude fungovat i na celoploÅ¡ném odkazu 149 block.querySelectorAll('.tour-card-button').forEach(function (button) { 150 button.addEventListener('click', function (e) { 151 e.stopPropagation(); // ZabránÃme propagaci kliknutà na celou kartu 152 }); 153 }); 154 155 // NaÄteme zájezdy už po naÄtenà stránky s výchozÃmi filtry 156 // POUZE pokud to nenà statický shortcode (tours-display-static) 157 if (!block.classList.contains('tours-display-static')) { 158 showLoading(toursContainer); 159 if (isFooter && footerSelectedTours) { 160 loadFilteredTours(block, originalFilters, userFilters, numberOfTours, displayMode, selectedTours, footerSelectedTours, filterConfig, filterModes, travelPackageFilter); 161 } else { 162 loadFilteredTours(block, originalFilters, userFilters, numberOfTours, displayMode, selectedTours, null, filterConfig, filterModes, travelPackageFilter); 163 } 164 } 165 }); 166}); 167 168function parseJsonAttribute(value) { 169 if (!value) { 170 return null; 171 } 172 173 try { 174 return JSON.parse(value); 175 } catch (error) { 176 return null; 177 } 178} 179 180function normalizeTravelPackageFilterValue(value) { 181 return value === 'true' || value === 'false' ? value : 'all'; 182} 183 184function normalizeArrayValues(values) { 185 if (Array.isArray(values)) { 186 return values.filter(Boolean); 187 } 188 189 if (typeof values === 'string' && values.length) {
190 return values.split(',').filter(Boolean); 191 } 192 193 return []; 194} 195 196function resolveSelectionMode(filterConfig, filterKey, buttonMode = '') { 197 if (buttonMode === 'single' || buttonMode === 'multi') { 198 return buttonMode; 199 } 200 201 if (!Array.isArray(filterConfig)) { 202 return 'single'; 203 } 204 205 const filter = filterConfig.find((item) => item.key === filterKey); 206 if (!filter) { 207 return 'single'; 208 } 209 210 return filter.selectionMode === 'single' ? 'single' : 'multi'; 211} 212 213function updateFilterButtonState(filterButtons, userFilters) { 214 const isAnyFilterActive = Object.values(userFilters).some((values) => Array.isArray(values) && values.length > 0); 215 216 filterButtons.forEach((button) => { 217 const filterKey = button.getAttribute('data-filter-key') || button.getAttribute('data-filter'); 218 const value = button.getAttribute('data-value') || ''; 219 220 let isActive = false; 221 222 if (filterKey === 'all') { 223 isActive = !isAnyFilterActive; 224 } else if (value === '') { 225 isActive = !userFilters[filterKey] || userFilters[filterKey].length === 0; 226 } else { 227 isActive = Array.isArray(userFilters[filterKey]) && userFilters[filterKey].includes(value); 228 } 229 230 button.classList.toggle('active', isActive); 231 }); 232} 233 234function updateFilterButtonAvailability(block, availableTerms, userFilters) { 235 if (!availableTerms || typeof availableTerms !== 'object') { 236 return; 237 } 238 239 const groups = block.querySelectorAll('.tours-display-filters-group'); 240 241 groups.forEach((group) => { 242 const filterKey = group.getAttribute('data-filter-group'); 243 if (!filterKey) { 244 return; 245 } 246 247 const allowedValues = Array.isArray(availableTerms[filterKey]) ? new Set(availableTerms[filterKey]) : new Set(); 248 const selectedValues = Array.isArray(userFilters[filterKey]) ? userFilters[filterKey] : []; 249 let visibleValueButtons = 0; 250 251 group.querySelectorAll('.tours-display-filter-btn').forEach((button) => { 252 const value = button.getAttribute('data-value') || ''; 253 254 if (value === '') { 255 button.style.display = ''; 256 return; 257 } 258 259 const shouldShow = allowedValues.has(value) || selectedValues.includes(value); 260 button.style.display = shouldShow ? '' : 'none'; 261 262 if (shouldShow) { 263 visibleValueButtons += 1; 264 } 265 }); 266 267 if (visibleValueButtons === 0 && selectedValues.length === 0) { 268 group.style.display = 'none'; 269 } else { 270 group.style.display = ''; 271 } 272 }); 273} 274 275function initShortcodeSearchForm(block, toursContainer, filterConfig = []) { 276 const form = block.querySelector('.tours-display-search-form'); 277 if (!form || !toursContainer || form.dataset.ajaxBound === '1') { 278 return; 279 } 280 281 form.dataset.ajaxBound = '1'; 282 const destinationSelect = form.querySelector('select.tours-display-search-destination'); 283 const monthSelect = form.querySelector('select.tours-display-search-month'); 284 const destinationMultiselect = initShortcodeMultiselectField(form, 'destination'); 285 const monthMultiselect = initShortcodeMultiselectField(form, 'month'); 286 const resetLink = form.querySelector('.tours-display-search-reset'); 287 const submitButton = form.querySelector('.tours-display-search-submit'); 288 let activeRequestId = 0; 289 290 const getSelectedValues = (multiselect, selectElement) => { 291 if (multiselect) { 292 return multiselect.getSelectedValues(); 293 } 294 295 if (selectElement && selectElement.value) { 296 return [selectElement.value]; 297 } 298 299 return []; 300 }; 301 302 const clearSelectedValues = (multiselect, selectElement) => { 303 if (multiselect) { 304 multiselect.clear(); 305 return; 306 } 307 308 if (selectElement) { 309 selectElement.value = ''; 310 } 311 }; 312 313 const runAjaxSearch = () => { 314 activeRequestId += 1; 315 const requestId = activeRequestId; 316 const params = new URLSearchParams(); 317 const count = block.getAttribute('data-shortcode-count') || ''; 318 const postType = block.getAttribute('data-shortcode-post-type') || ''; 319 const baseTaxonomy = block.getAttribute('data-shortcode-base-taxonomy') || ''; 320 const baseTerm = block.getAttribute('data-shortcode-base-term') || ''; 321 const destinationTaxonomy = block.getAttribute('data-shortcode-destination-taxonomy') || ''; 322 323 if (count) { 324 params.append('count', count); 325 } 326 if (postType) { 327 params.append('postType', postType); 328 } 329 if (baseTaxonomy) { 330 params.append('baseTaxonomy', baseTaxonomy); 331 } 332 if (baseTerm) { 333 params.append('baseTerm', baseTerm); 334 } 335 if (destinationTaxonomy) { 336 params.append('destinationTaxonomy', destinationTaxonomy); 337 } 338 339 const selectedDestinations = getSelectedValues(destinationMultiselect, destinationSelect); 340 const selectedMonths = getSelectedValues(monthMultiselect, monthSelect); 341 342 selectedDestinations.forEach((value) => { 343 params.append('destination[]', value); 344 }); 345 selectedMonths.forEach((value) => { 346 params.append('month[]', value); 347 }); 348 349 form.classList.add('is-loading'); 350 if (submitButton) { 351 submitButton.disabled = true; 352 } 353 showLoadingOverlay(toursContainer); 354 355 fetch(window.toursDisplayData.restUrl + 'tours/shortcode-search?' + params.toString(), { 356 method: 'GET', 357 headers: { 358 'Content-Type': 'application/json', 359 'X-WP-Nonce': window.toursDisplayData.nonce 360 } 361 }) 362 .then(response => { 363 if (!response.ok) { 364 throw new Error('Network response was not ok'); 365 } 366 return response.json(); 367 }) 368 .then((responseData) => { 369 if (requestId !== activeRequestId) { 370 return; 371 } 372 373 const tours = Array.isArray(responseData?.tours) ? responseData.tours : []; 374 375 toursContainer.innerHTML = ''; 376 377 if (!tours.length) { 378 toursContainer.innerHTML = '<p class="tours-display-no-results">Žádné zájezdy nebyly nalezeny.</p>'; 379 return; 380 } 381
382 tours.forEach((tour) => { 383 const tourCard = createTourCard(tour, filterConfig); 384 toursContainer.appendChild(tourCard); 385 }); 386 }) 387 .catch(() => { 388 if (requestId !== activeRequestId) { 389 return; 390 } 391 392 toursContainer.innerHTML = '<p class="tours-display-no-results">DoÅ¡lo k chybÄ pÅi naÄÃtánà zájezdů. Zkuste to prosÃm znovu.</p>'; 393 }) 394 .finally(() => { 395 if (requestId !== activeRequestId) { 396 return; 397 } 398 399 hideLoadingOverlay(toursContainer); 400 form.classList.remove('is-loading'); 401 if (submitButton) { 402 submitButton.disabled = false; 403 } 404 }); 405 }; 406 407 form.addEventListener('submit', function (event) { 408 event.preventDefault(); 409 runAjaxSearch(); 410 }); 411 412 if (destinationMultiselect) { 413 destinationMultiselect.bindChange(runAjaxSearch); 414 } else if (destinationSelect) { 415 destinationSelect.addEventListener('change', function () { 416 runAjaxSearch(); 417 }); 418 } 419 420 if (monthMultiselect) { 421 monthMultiselect.bindChange(runAjaxSearch); 422 } else if (monthSelect) { 423 monthSelect.addEventListener('change', function () { 424 runAjaxSearch(); 425 }); 426 } 427 428 if (resetLink) { 429 resetLink.addEventListener('click', function (event) { 430 event.preventDefault(); 431 432 clearSelectedValues(destinationMultiselect, destinationSelect); 433 clearSelectedValues(monthMultiselect, monthSelect); 434 closeAllShortcodeMultiselectMenus(form); 435 436 runAjaxSearch(); 437 }); 438 } 439} 440 441function initShortcodeMultiselectField(form, fieldName) { 442 const field = form.querySelector(`[data-shortcode-multiselect-field="${fieldName}"]`); 443 if (!field) { 444 return null; 445 } 446 447 const toggleButton = field.querySelector('[data-shortcode-multiselect-toggle]'); 448 const dropdownMenu = field.querySelector('[data-shortcode-multiselect-menu]'); 449 const summaryNode = field.querySelector('[data-shortcode-multiselect-summary]'); 450 const countNode = field.querySelector('[data-shortcode-multiselect-count]'); 451 const checkboxes = Array.from(field.querySelectorAll(`input[type="checkbox"].tours-display-search-${fieldName}`)); 452 const placeholder = field.getAttribute('data-placeholder') || ''; 453 const optionLabelSelector = '.tours-display-search-multiselect-option-label'; 454 455 const getOptionLabels = () => ( 456 checkboxes 457 .filter((checkbox) => checkbox.checked) 458 .map((checkbox) => { 459 const parentLabel = checkbox.closest('label'); 460 const labelNode = parentLabel ? parentLabel.querySelector(optionLabelSelector) : null; 461 const rawLabel = labelNode ? labelNode.textContent : checkbox.value; 462 return rawLabel.replace(/^\s*-\s*/, '').trim(); 463 }) 464 .filter(Boolean) 465 ); 466 467 const updateSummary = () => { 468 const selectedLabels = getOptionLabels(); 469 const selectedCount = selectedLabels.length; 470 let summaryText = placeholder; 471 472 if (selectedCount === 1) { 473 summaryText = selectedLabels[0]; 474 } else if (selectedCount > 1) { 475 summaryText = `${selectedLabels[0]} +${selectedCount - 1}`; 476 } 477 478 if (summaryNode) { 479 summaryNode.textContent = summaryText; 480 } 481 482 if (countNode) { 483 countNode.textContent = selectedCount ? String(selectedCount) : ''; 484 countNode.classList.toggle('has-value', selectedCount > 0); 485 } 486 487 field.classList.toggle('has-selection', selectedCount > 0); 488 }; 489 490 const setMenuState = (open) => { 491 if (!toggleButton || !dropdownMenu) { 492 return; 493 } 494 495 field.classList.toggle('is-open', open); 496 toggleButton.setAttribute('aria-expanded', open ? 'true' : 'false'); 497 dropdownMenu.hidden = !open; 498 }; 499 500 if (toggleButton && dropdownMenu) { 501 toggleButton.addEventListener('click', function () { 502 const shouldOpen = !field.classList.contains('is-open'); 503 closeAllShortcodeMultiselectMenus(form, field); 504 setMenuState(shouldOpen); 505 }); 506 507 document.addEventListener('click', function (event) { 508 if (!field.contains(event.target)) { 509 setMenuState(false); 510 } 511 }); 512 513 document.addEventListener('keydown', function (event) { 514 if (event.key === 'Escape') { 515 setMenuState(false); 516 } 517 }); 518 } 519 520 updateSummary(); 521 522 return { 523 bindChange: (callback) => {
524 checkboxes.forEach((checkbox) => { 525 checkbox.addEventListener('change', function () { 526 updateSummary(); 527 callback(); 528 }); 529 }); 530 }, 531 clear: () => { 532 checkboxes.forEach((checkbox) => { 533 checkbox.checked = false; 534 }); 535 updateSummary(); 536 }, 537 getSelectedValues: () => ( 538 checkboxes 539 .filter((checkbox) => checkbox.checked) 540 .map((checkbox) => checkbox.value) 541 .filter(Boolean) 542 ), 543 }; 544} 545 546function closeAllShortcodeMultiselectMenus(scope, keepOpenField = null) { 547 const container = scope || document; 548 const fields = container.querySelectorAll('.tours-display-search-multiselect'); 549 550 fields.forEach((field) => { 551 if (keepOpenField && field === keepOpenField) { 552 return; 553 } 554 555 const toggleButton = field.querySelector('[data-shortcode-multiselect-toggle]'); 556 const dropdownMenu = field.querySelector('[data-shortcode-multiselect-menu]'); 557 558 field.classList.remove('is-open'); 559 if (toggleButton) { 560 toggleButton.setAttribute('aria-expanded', 'false'); 561 } 562 if (dropdownMenu) { 563 dropdownMenu.hidden = true; 564 } 565 }); 566} 567 568function showLoadingOverlay(container) { 569 if (!container) { 570 return; 571 } 572 573 container.classList.add('tours-display-is-loading'); 574 let overlay = container.querySelector('.tours-loading-overlay'); 575 576 if (!overlay) { 577 overlay = document.createElement('div'); 578 overlay.className = 'tours-loading-overlay'; 579 overlay.innerHTML = '<div class="tours-loading-spinner"></div>'; 580 container.appendChild(overlay); 581 } 582} 583 584function hideLoadingOverlay(container) { 585 if (!container) { 586 return; 587 } 588 589 container.classList.remove('tours-display-is-loading'); 590 const overlay = container.querySelector('.tours-loading-overlay'); 591 if (overlay) { 592 overlay.remove(); 593 } 594} 595 596/** 597 * Zobrazà naÄÃtacà indikátor v kontejneru zájezdů 598 */ 599function showLoading(container) { 600 // VytvoÅÃme a zobrazÃme loading indikátor 601 const loadingDiv = document.createElement('div'); 602 loadingDiv.className = 'tours-loading'; 603 loadingDiv.innerHTML = '<div class="tours-loading-spinner"></div>'; 604 605 // VyÄistÃme kontejner a pÅidáme loading 606 container.innerHTML = ''; 607 container.appendChild(loadingDiv); 608} 609 610/** 611 * NaÄte nové zájezdy podle filtraÄnÃch parametrů 612 */ 613function loadFilteredTours(block, originalFilters, userFilters, numberOfTours, displayMode, selectedTours, footerSelectedTours, filterConfig = [], filterModes = {}, travelPackageFilter = 'all') { 614 const blockClass = block.className; 615 const isFooter = blockClass.includes('tours-display-footer-block'); 616 617 // ZÃskáme kontejner pro zájezdy - buÄ standardnà nebo footer verze 618 const toursContainer = block.querySelector('.tours-display-container') || 619 block.querySelector('.tours-display-footer-container'); 620 621 // Parametry pro API request 622 const params = new URLSearchParams(); 623 params.append('limit', numberOfTours); 624 params.append('strictFiltering', 'true'); // Nový parametr pro striktnà filtrovánà 625 const normalizedTravelPackageFilter = normalizeTravelPackageFilterValue(travelPackageFilter); 626 627 const hasDynamicConfig = Array.isArray(filterConfig) && filterConfig.length > 0; 628 629 if (hasDynamicConfig) { 630 const gutenbergPayload = {}; 631 const userPayload = {}; 632 const modePayload = {}; 633 634 for (let key in originalFilters) { 635 if (originalFilters[key] && originalFilters[key].length > 0) { 636 gutenbergPayload[key] = originalFilters[key]; 637 } 638 } 639 640 for (let key in userFilters) { 641 if (userFilters[key] && userFilters[key].length > 0) { 642 userPayload[key] = userFilters[key]; 643 } 644 } 645 646 for (let key in filterModes) { 647 if (filterModes[key] === 'single' || filterModes[key] === 'multi') { 648 modePayload[key] = filterModes[key]; 649 } 650 } 651 652 if (Object.keys(gutenbergPayload).length) { 653 params.append('gutenbergFilters', JSON.stringify(gutenbergPayload)); 654 } 655 656 if (Object.keys(userPayload).length) { 657 params.append('userFilters', JSON.stringify(userPayload)); 658 } 659 660 if (Object.keys(modePayload).length) { 661 params.append('selectionModes', JSON.stringify(modePayload)); 662 } 663 664 if (displayMode === 'latest') { 665 params.append('includeFilters', '1'); 666 } 667 } else { 668 // Legacy fallback for older block markup/scripts. 669 for (let key in originalFilters) { 670 if (originalFilters[key] && originalFilters[key].length > 0) { 671 params.append('gutenberg' + capitalizeFirstLetter(filterKeyToApi(key)), JSON.stringify(originalFilters[key])); 672 } 673 } 674 675 for (let key in userFilters) { 676 if (userFilters[key] && userFilters[key].length > 0) { 677 params.append('user' + capitalizeFirstLetter(filterKeyToApi(key)), JSON.stringify(userFilters[key])); 678 } 679 } 680 } 681
682 if (normalizedTravelPackageFilter !== 'all') { 683 params.append('travelPackageFilter', normalizedTravelPackageFilter); 684 } 685 686 // Pokud jsme na stránce zájezdu a je vybraný mode "related", pÅidáme aktuálnà ID 687 if (displayMode === 'related') { 688 const currentPostId = getCurrentPostId(); 689 if (currentPostId) { 690 params.append('currentPostId', currentPostId); 691 } 692 } 693 694 // URL pro API request 695 let apiUrl = ''; 696 697 // OPRAVA: Pro jednotlivé režimy zobrazenà použijeme různé endpointy 698 if (displayMode === 'random') { 699 apiUrl = window.toursDisplayData.restUrl + 'tours/random?' + params.toString(); 700 } else if (displayMode === 'related') { 701 apiUrl = window.toursDisplayData.restUrl + 'tours/related?' + params.toString(); 702 } else if (isFooter && footerSelectedTours) { 703 // DÅ®LEŽITÃ: Pro patiÄku s manuálnÃm výbÄrem použijeme endpoint tours s pÅedánÃm vybraných ID z patiÄky 704 apiUrl = window.toursDisplayData.restUrl + 'tours?ids=' + footerSelectedTours; 705 } else if (displayMode === 'selected' && selectedTours) { 706 // Pro blok v režimu 'selected' použijeme endpoint tours s pÅedánÃm vybraných ID 707 apiUrl = window.toursDisplayData.restUrl + 'tours?ids=' + selectedTours; 708 } else { 709 // Pro 'latest' a fallback pro dalšà režimy použijeme acf-filter 710 apiUrl = window.toursDisplayData.restUrl + 'tours/acf-filter?' + params.toString(); 711 } 712 713 // Provedeme fetch request 714 fetch(apiUrl, { 715 method: 'GET', 716 headers: { 717 'Content-Type': 'application/json', 718 'X-WP-Nonce': window.toursDisplayData.nonce 719 } 720 }) 721 .then(response => { 722 if (!response.ok) { 723 throw new Error('Network response was not ok'); 724 } 725 return response.json(); 726 }) 727 .then((responseData) => { 728 let tours = responseData; 729 let availableTerms = null; 730 731 if (!Array.isArray(responseData) && responseData && Array.isArray(responseData.tours)) { 732 tours = responseData.tours; 733 availableTerms = responseData.availableTerms && typeof responseData.availableTerms === 'object' 734 ? responseData.availableTerms 735 : null; 736 } 737 738 if (availableTerms) { 739 updateFilterButtonAvailability(block, availableTerms, userFilters); 740 updateFilterButtonState(block.querySelectorAll('.tours-display-filter-btn'), userFilters); 741 } 742 743 // VyÄistÃme kontejner 744 toursContainer.innerHTML = ''; 745 746 if (tours.length === 0) { 747 // Pokud nejsou žádné zájezdy, zobrazÃme zprávu 748 toursContainer.innerHTML = '<p class="tours-display-no-results">Žádné zájezdy nebyly nalezeny s vybranými filtry.</p>'; 749 750 return; 751 } 752 753 // VykreslÃme nové karty zájezdů 754 tours.forEach(tour => { 755 const tourCard = createTourCard(tour, filterConfig); 756 toursContainer.appendChild(tourCard); 757 }); 758 759 760 // Aplikujeme animace na nové karty 761 setTimeout(() => { 762 toursContainer.querySelectorAll('.tour-card').forEach(card => { 763 card.style.animation = 'fadeIn 0.5s ease-out'; 764 }); 765 }, 10); 766 }) 767 .catch(error => { 768 toursContainer.innerHTML = '<p class="tours-display-no-results">DoÅ¡lo k chybÄ pÅi naÄÃtánà zájezdů. Zkuste to prosÃm znovu.</p>'; 769 }); 770} 771 772 773/** 774 * VytvoÅà HTML element s kartou zájezdu 775 */ 776function createTourCard(tour, filterConfig = []) { 777 // VytvoÅÃme nový element pro kartu 778 const card = document.createElement('div'); 779 card.className = 'tour-card'; 780 card.dataset.id = tour.id; 781 782 // Typ obsahu držà i karta stavÄná v JS - server ho na kartu dává taky 783 // (class-tours-display.php), CSS podle nÄj skrývá tabulku u plaveb na mÃru. 784 if (tour.post_type) { 785 card.dataset.postType = tour.post_type; 786 } 787 788 // PÅidáme data atributy pro filtraci 789 if (tour.category && tour.category.id) { 790 card.dataset.category = tour.category.id; 791 } 792 793 // PÅidáme data atributy pro ACF filtry 794 if (tour.acf_filters) { 795 for (const [key, values] of Object.entries(tour.acf_filters)) { 796 if (values && values.length) { 797 card.dataset[key] = Array.isArray(values) ? values.join(',') : values; 798 } 799 } 800 } 801 802 // Odkaz pÅes celou kartu 803 const fullLink = document.createElement('a'); 804 fullLink.className = 'tour-card-full-link'; 805 fullLink.href = tour.permalink; 806 fullLink.setAttribute('aria-label', tour.title); 807 card.appendChild(fullLink); 808 809 // Kontejner pro obrázek 810 const imageContainer = document.createElement('div'); 811 imageContainer.className = 'tour-card-image-container'; 812 813 // Obrázek 814 const image = document.createElement('img'); 815 image.src = tour.image; 816 image.alt = tour.title; 817 image.className = 'tour-card-image'; 818 imageContainer.appendChild(image); 819 820 // Badge s kategorià - prioritnÄ podle poÅadà filtrů z konfigurace. 821 let badgeAdded = false;
822 if (tour.filter_labels) { 823 const orderedFilterKeys = Array.isArray(filterConfig) && filterConfig.length 824 ? filterConfig.map((filter) => filter.key) 825 : Object.keys(tour.filter_labels); 826 827 for (const filterKey of orderedFilterKeys) { 828 const labelsBySlug = tour.filter_labels?.[filterKey]; 829 if (!labelsBySlug || !Object.keys(labelsBySlug).length) { 830 continue; 831 } 832 833 const selectedValues = Array.isArray(tour.acf_filters?.[filterKey]) 834 ? tour.acf_filters[filterKey] 835 : (tour.acf_filters?.[filterKey] ? [tour.acf_filters[filterKey]] : []); 836 837 const preferredSlug = selectedValues.length ? selectedValues[0] : ''; 838 const fallbackLabel = Object.values(labelsBySlug)[0]; 839 const finalLabel = preferredSlug && labelsBySlug[preferredSlug] 840 ? labelsBySlug[preferredSlug] 841 : fallbackLabel; 842 843 if (!finalLabel) { 844 continue; 845 } 846 847 const badge = document.createElement('div'); 848 badge.className = 'tour-card-category-badge'; 849 badge.textContent = finalLabel; 850 imageContainer.appendChild(badge); 851 badgeAdded = true; 852 break; 853 } 854 } 855 856 // Fallback na původnà kategorii 857 if (!badgeAdded && tour.category && tour.category.name) { 858 const badge = document.createElement('div'); 859 badge.className = 'tour-card-category-badge'; 860 badge.textContent = tour.category.name; 861 imageContainer.appendChild(badge); 862 } 863 864 card.appendChild(imageContainer); 865 866 // Obsah karty 867 const content = document.createElement('div'); 868 content.className = 'tour-card-content'; 869 870 // Nadpis 871 const title = document.createElement('h3'); 872 title.className = 'tour-card-title'; 873 title.textContent = tour.title; 874 content.appendChild(title); 875 876 // Tabulka s informacemi (termÃn a cena) 877 const table = document.createElement('table'); 878 table.className = 'tour-card-info-table'; 879 let tableHtml = '<tbody>'; 880 881 // PÅidáme Åádky do tabulky podle typu zájezdu a dostupných dat 882 if (tour.is_travel_package) { 883 // Pro cestovatelský balÃÄek 884 tableHtml += ` 885 <tr> 886 <th>TermÃn</th> 887 <td>${tour.date || 'Kontaktujte nás'}</td> 888 </tr> 889 `; 890 891 // Délka programu 892 if (tour.travel_package_data && tour.travel_package_data.delka_programu) { 893 tableHtml += ` 894 <tr> 895 <th>Délka programu</th> 896 <td>${tour.travel_package_data.delka_programu}</td> 897 </tr> 898 `; 899 } 900 901 // Cena pro 2 osoby 902 if (tour.travel_package_data && tour.travel_package_data.cena_2_osoby) { 903 tableHtml += ` 904 <tr> 905 <th>Cena pro 2 osoby</th> 906 <td><span class="tour-price-badge">${tour.travel_package_data.cena_2_osoby}</span></td> 907 </tr> 908 `; 909 } 910 911 // Cena pro 3 osoby 912 if (tour.travel_package_data && tour.travel_package_data.cena_3_osoby) { 913 tableHtml += ` 914 <tr> 915 <th>Cena pro 3 osoby</th> 916 <td><span class="tour-price-badge">${tour.travel_package_data.cena_3_osoby}</span></td> 917 </tr> 918 `; 919 } 920 921 // Cena pro 4 a vÃce osob 922 if (tour.travel_package_data && tour.travel_package_data.cena_4_osoby) { 923 tableHtml += ` 924 <tr> 925 <th>Cena pro 4 a vÃce osob</th> 926 <td><span class="tour-price-badge">${tour.travel_package_data.cena_4_osoby}</span></td> 927 </tr> 928 `; 929 } 930 } else { 931 // Pro bÄžný zájezd 932 const useTyp287Default = tour.typ_287_listing_fallback === true; 933 const useFlightPriceColumn = !useTyp287Default && tour.use_flight_price_listing === true; 934 935 tableHtml += ` 936 <tr class="tour-table-header"> 937 <th>TermÃn</th> 938 <th>${useTyp287Default ? 'Cena' : (useFlightPriceColumn ? 'Cena s letenkou' : 'Cena bez letenky')}</th> 939 </tr> 940 `; 941 942 if (useTyp287Default) { 943 tableHtml += ` 944 <tr class="tour-date-row"> 945 <td>BÄhem celého roku</td> 946 <td><span class="tour-price-badge">na vyžádánÃ</span></td> 947 </tr> 948 `; 949 } 950 // PÅidáme termÃny z ACF 951 else if (tour.tour_dates_data && tour.tour_dates_data.length) { 952 tour.tour_dates_data.forEach(date => { 953 let dateDisplay = 'Kontaktujte nás'; 954 if (date.tour_start_date) { 955 // Formátovánà data bez poÄáteÄnÃch nul 956 const formatDateWithoutLeadingZeros = (dateStr) => { 957 // Kontrola formátu dd.mm.yyyy 958 const dateParts = dateStr.split('.'); 959 if (dateParts.length === 3) { 960 // OdstranÄnà poÄáteÄnÃch nul 961 const day = parseInt(dateParts[0], 10);
962 const month = parseInt(dateParts[1], 10); 963 const year = dateParts[2]; 964 return `${day}.${month}.${year}`; 965 } 966 return dateStr; // Vrátit původnà ÅetÄzec, pokud formát neodpovÃdá 967 }; 968 969 const startDateFormatted = formatDateWithoutLeadingZeros(date.tour_start_date); 970 971 if (date.tour_end_date) { 972 const endDateFormatted = formatDateWithoutLeadingZeros(date.tour_end_date); 973 dateDisplay = `${startDateFormatted} - ${endDateFormatted}`; 974 } else { 975 dateDisplay = startDateFormatted; 976 } 977 } 978 979 tableHtml += ` 980 <tr class="tour-date-row"> 981 <td>${dateDisplay}</td> 982 <td> 983 `; 984 985 if (useFlightPriceColumn) { 986 if (date.tour_price_with_flight) { 987 let priceText = date.tour_price_with_flight; 988 let badgeClass = "tour-price-badge"; 989 990 if (date.tour_discount_note_with_flight) { 991 priceText = priceText + '<br>' + date.tour_discount_note_with_flight; 992 badgeClass += " tour-price-badge-bold"; 993 } 994 995 if (date.tour_price_supplement_with_flight) { 996 const tempDiv = document.createElement('div'); 997 tempDiv.innerHTML = date.tour_price_supplement_with_flight; 998 const supplement = tempDiv.textContent || tempDiv.innerText || ''; 999 priceText = priceText + '<br>' + supplement; 1000 badgeClass += " tour-price-badge-bold"; 1001 } 1002 1003 tableHtml += `<span class="${badgeClass}">${priceText}</span>`; 1004 } else if (date.tour_price_supplement_with_flight) { 1005 const tempDiv = document.createElement('div'); 1006 tempDiv.innerHTML = date.tour_price_supplement_with_flight; 1007 const supplement = tempDiv.textContent || tempDiv.innerText || ''; 1008 tableHtml += `<span class="tour-price-badge tour-price-badge-bold">${supplement}</span>`; 1009 } else { 1010 tableHtml += 'Na vyžádánÃ'; 1011 } 1012 } else { 1013 if (date.tour_price) { 1014 // Pokud máme vyplnÄnou cenu 1015 let priceText = date.tour_price; 1016 let badgeClass = "tour-price-badge"; 1017 1018 if (date.tour_discount_note) { 1019 priceText = priceText + '<br>' + date.tour_discount_note; 1020 badgeClass += " tour-price-badge-bold"; 1021 } 1022 1023 if (date.tour_price_supplement) { 1024 // Odstranit HTML tagy z doplÅku ceny a pÅidat pod cenu 1025 const tempDiv = document.createElement('div'); 1026 tempDiv.innerHTML = date.tour_price_supplement; 1027 const supplement = tempDiv.textContent || tempDiv.innerText || ''; 1028 priceText = priceText + '<br>' + supplement; 1029 1030 // PÅidat tÅÃdu pro ztuÄnÄnÃ, pokud existuje doplnÄk 1031 badgeClass += " tour-price-badge-bold"; 1032 } 1033 1034 tableHtml += `<span class="${badgeClass}">${priceText}</span>`; 1035 } else if (date.tour_price_supplement) { 1036 // Pokud nemáme cenu, ale máme doplnÄk ceny, použijeme ho mÃsto "Na vyžádánÃ" 1037 const tempDiv = document.createElement('div'); 1038 tempDiv.innerHTML = date.tour_price_supplement; 1039 const supplement = tempDiv.textContent || tempDiv.innerText || ''; 1040 1041 // Vždy ztuÄnit, když máme jen doplnÄk bez ceny 1042 tableHtml += `<span class="tour-price-badge tour-price-badge-bold">${supplement}</span>`;
1043 } else { 1044 // Pokud nemáme ani cenu ani doplnÄk 1045 tableHtml += 'Na vyžádánÃ'; 1046 } 1047 } 1048 1049 tableHtml += ` 1050 </td> 1051 </tr> 1052 `; 1053 }); 1054 } else { 1055 // Jednoduchý Åádek s původnÃmi daty 1056 tableHtml += ` 1057 <tr> 1058 <td>${tour.date || 'Kontaktujte nás'}</td> 1059 <td><span class="tour-price-badge">${tour.price || 'Na vyžádánÃ'}</span></td> 1060 </tr> 1061 `; 1062 } 1063 } 1064 1065 tableHtml += '</tbody>'; 1066 table.innerHTML = tableHtml; 1067 content.appendChild(table); 1068 1069 // Podnadpis 1070 if (tour.subtitle) { 1071 const subtitle = document.createElement('div'); 1072 subtitle.className = 'tour-card-subtitle'; 1073 subtitle.innerHTML = tour.subtitle; 1074 content.appendChild(subtitle); 1075 } 1076 1077 // Popis 1078 if (tour.description) { 1079 const description = document.createElement('div'); 1080 description.className = 'tour-card-description'; 1081 description.innerHTML = tour.description; 1082 content.appendChild(description); 1083 } 1084 1085 // TlaÄÃtko 1086 const button = document.createElement('a'); 1087 button.className = 'tour-card-button'; 1088 button.href = tour.permalink; 1089 button.textContent = 'Detail zájezdu'; 1090 content.appendChild(button); 1091 1092 card.appendChild(content); 1093 1094 return card; 1095} 1096 1097/** 1098 * ZÃská ID aktuálnÃho pÅÃspÄvku, pokud jsme na stránce zájezdu 1099 */ 1100function getCurrentPostId() { 1101 // ZkusÃme najÃt meta tag s ID pÅÃspÄvku 1102 const postIdMeta = document.querySelector('meta[property="og:id"]'); 1103 if (postIdMeta) { 1104 return postIdMeta.getAttribute('content'); 1105 } 1106 1107 // ZkusÃme najÃt body tag s tÅÃdou obsahujÃcà ID 1108 const bodyClasses = document.body.className.split(' '); 1109 for (let i = 0; i < bodyClasses.length; i++) { 1110 if (bodyClasses[i].indexOf('postid-') === 0) { 1111 return bodyClasses[i].replace('postid-', ''); 1112 } 1113 } 1114 1115 return null; 1116} 1117 1118/** 1119 * PÅevede klÃÄ filtru na formát pro API 1120 */ 1121function filterKeyToApi(key) { 1122 switch (key) { 1123 case 'typ_zajezdu': 1124 return 'typZajezdu'; 1125 case 'zajezd_rozdeleni': 1126 return 'zajezdRozdeleni'; 1127 case 'zajezd_destinace': 1128 return 'zajezdDestinace'; 1129 default: 1130 return key; 1131 } 1132} 1133 1134/** 1135 * Pomocná funkce - pÅevede prvnà pÃsmeno na velké 1136 */ 1137function capitalizeFirstLetter(string) { 1138 return string.charAt(0).toUpperCase() + string.slice(1); 1139}
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.