PageSourceSearch

https://www.inspira-sailing.cz/wp-content/plugins/tours-display/src/frontend.js?ver=1.0.58

js inspira-sailing.cz collected 2026-10-08 00:36:52 UTC 43,150 bytes, 1,139 lines download raw bytes

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.