PageSourceSearch

https://canoetribe.com/wp-content/plugins/tbx-plugin/blox/calendarv2/script.js?ver=1790633312

js canoetribe.com collected 2026-09-29 00:54:31 UTC 42,916 bytes, 975 lines download raw bytes

1// const { validate } = require('uuid');
2
3tb.onRootElementReady((tbRootElement) => {
4    const calendarElements = tbRootElement.querySelectorAll('.tb-calendar-v2');
5    calendarElements.forEach((calendarElement) => {
6        const calendar = new CalendarV2(calendarElement);
7    });
8});
9
10// Mirrors calendar_format_price() in inc/calendar/calendar.php: whole euros stay
11// whole, a fractional amount gets two decimals with a comma. Core divides an
12// accommodation price by the party size, so a total can carry cents.
13function formatPrice(value) {
14    return Number.isInteger(value) ? String(value) : value.toFixed(2).replace('.', ',');
15}
16
17class CalendarV2 {
18    constructor(element) {
19        this.element = element;
20        this.parentSection = this.element.closest('.tb-calendarv2');
21        this.departureElements = this.element.querySelectorAll('.tb-calendar-v2__departure');
22        this.sectionTogglers = this.element.querySelectorAll('.section-toggler:not(.section-toggler--fake)');
23        this.expansionToggles = this.element.querySelectorAll('.tb-calendar-v2-departure__expand-button.mobile-only');
24        this.showPrevButtons = this.element.querySelectorAll('.tb-calendar-v2__show-prev-button');
25        this.showNextButtons = this.element.querySelectorAll('.tb-calendar-v2__show-next-button');
26        this.filters = this.parentSection.querySelectorAll('.tb-calendar-v2__filter');
27
28        // Store bound function references for proper event listener management
29        this.boundLoadMonth = this.loadMonth.bind(this);
30
31        this.ajaxUrl = tb.AJAX_URL;
32        // if the calendar is loaded in a connector block, we must use the admin-ajax.php url of the connector url
33        if (this.element.closest('.tbx-connector-content')) {
34            if (tb.CONNECTOR_AJAX_URL) {
35                this.ajaxUrl = tb.CONNECTOR_AJAX_URL;
36            }
37        }
38        this.noParticipantsTitle = this.parentSection.getAttribute('data-no-participants-title');
39        this.noParticipantsText = this.parentSection.getAttribute('data-no-participants-text');
40        this.packageIds = this.parentSection.getAttribute('data-package-ids');
41        this.fieldPackages = this.parentSection.getAttribute('data-field-packages');
42        this.showPreregItems = this.parentSection.getAttribute('data-show-prereg-items');
43        this.allowCollapseConsecutiveSoldOutItems = this.parentSection.getAttribute(
44            'data-allow-collapse-consecutive-sold-out-items',
45        );
46        this.showParticipants = this.parentSection.getAttribute('data-show-participants');
47        this.showDepartureHours = this.parentSection.getAttribute('data-show-departure-hours');
48        this.showMonthsToggle = this.parentSection.getAttribute('data-show-months-toggle');
49        this.showWaitingListButtons = this.parentSection.getAttribute('data-show-waiting-list-buttons');
50        this.soldOutCta = this.parentSection.getAttribute('data-sold-out-cta');
51        this.defaultGroupSize = this.parentSection.getAttribute('data-default-group-size');
52        this.calendarData = this.element.getAttribute('data-calendar-data');
53        this.summaryContent = this.element.getAttribute('data-summary-content');
54        this.summaryInclusive = this.element.getAttribute('data-summary-inclusive');
55        this.skeleton = this.parentSection.querySelector('.tb-skeleton');
56        this.hiddenInputs = this.parentSection.querySelectorAll('.keep-me-updated-hidden');
57        this.generalKeepMeUpdatedButton = this.parentSection.querySelector('.tb-calendar-v2__keep-me-updated');
58        this.keepMeUpdatedButtons = this.parentSection.querySelectorAll(
59            '.tb-calendar-v2-departure__keep-me-updated button',
60        );
61        this.showQuoteRequest = this.parentSection.getAttribute('data-show-quote-request');
62        this.showPriceToggle = this.parentSection.getAttribute('data-show-price-toggle');
63        this.sortFilterOptionsByAZOrder = this.parentSection.getAttribute('data-sort-filter-options-by-az-order');
64        this.mergeDuplicateFilters = this.parentSection.getAttribute('data-merge-duplicate-filters');
65        this.brands = this.parentSection.getAttribute('data-brands');
66        this.language = this.parentSection.getAttribute('data-language');
67        this.keepUpdatedForms = this.parentSection.querySelectorAll('.tb-form--calendar-keep-updated');
68        this.keepUpdatedBrand = '';
69        this.keepUpdatedBrands = [];
70        this.keepUpdatedBrandID = '';
71        this.keepUpdatedPackage = '';
72        this.keepUpdatedProject = '';
73        this.keepUpdatedTour = '';
74        this.keepUpdatedDeparture = '';
75        this.keepUpdatedDestination = '';
76
77        this.expandedDeparture = null;
78        this.expandedBrand = '';
79        this.expandedPackage = '';
80        this.expandedProject = '';
81        this.expandedTour = '';
82        this.expandedDeparture = '';
83        this.expandedDestination = '';
84
85        this.allYears = [];
86        this.allMonths = [];
87        this.activeYear = 0;
88        this.activeMonth = 0;
89        this.maxYear = 0;
90        this.maxMonth = 0;
91        this.minYear = 0;
92        this.minMonth = 0;
93        this.activeFilters = [];
94
95        this.xhr = new XMLHttpRequest();
96        this.leadxhr = new XMLHttpRequest();
97        this.participantsXhr = new XMLHttpRequest();
98        
99        // Booking options are picked per departure, so the calendar opens on the
100        // visitor's brand/destination choice only — nothing to precompute here.
101        this.loadInitialContent([]);
102        
103        // Only need initForms() here;
103 calendarv2 binds its own buttons.
104        // skipButtons prevents re-binding the global menu button (popup opened twice).
105        this.keepMeUpdated = new KeepMeUpdated({ skipButtons: true });
106    }
107    init() {
108        this.parentSection = this.element.closest('.tb-calendarv2');
109        this.departureElements = this.element.querySelectorAll('.tb-calendar-v2__departure');
110        this.sectionTogglers = this.element.querySelectorAll('.section-toggler:not(.section-toggler--fake)');
111        this.expansionToggles = this.element.querySelectorAll('.tb-calendar-v2-departure__expand-button');
112        this.showPrevButtons = this.element.querySelectorAll('.tb-calendar-v2__show-prev-button');
113        this.showNextButtons = this.element.querySelectorAll('.tb-calendar-v2__show-next-button');
114        this.filters = this.parentSection.querySelectorAll('.tb-calendar-v2__filter');
115        this.packageIds = this.parentSection.getAttribute('data-package-ids');
116        this.fieldPackages = this.parentSection.getAttribute('data-field-packages');
117        this.calendarData = this.element.getAttribute('data-calendar-data');
118        this.skeleton = this.parentSection.querySelector('.tb-skeleton');
119        this.hiddenInputs = this.parentSection.querySelectorAll('.keep-me-updated-hidden');
120        this.keepMeUpdatedButtons = this.parentSection.querySelectorAll('.tb-calendar-v2-departure__keep-me-updated');
121        this.keepUpdatedForms = this.parentSection.querySelectorAll('.tb-form--calendar-keep-updated');
122
123        this.allYears = [];
124        this.allMonths = [];
125        this.activeYear = 0;
126        this.activeMonth = 0;
127        this.maxYear = 0;
128        this.maxMonth = 0;
129        this.minYear = 0;
130        this.minMonth = 0;
131
132        if (tbPopup) {
133            tbPopup.enableTriggers(this.element);
134        }
135        this.setFilters();
136        this.initDepartureOptions();
137        this.initDepartureExpansionBoxes();
138        this.enableJs();
139        this.getActiveYearAndMonth();
140        this.getMaxYearAndMonth();
141        this.setActiveMonthOnInput();
142        this.setHiddenInputs();
143        // initKeepMeUpdatedForms();
144        this.keepMeUpdated.initForms();
145        this.resizeChildrenOnScreenResize();
146        initCustomSelects();
147        initSectionTogglers();
148    }
149
150    // Booking options live inside the departure's own details panel: picking one
151    // reprices that departure and nothing else. Every option carries its difference
152    // against the cheapest bookable one in its category, which is what the listed
153    // price already includes, so the categories simply add up.
154    initDepartureOptions() {
155        this.departureElements.forEach((departureElement) => {
156            const options = departureElement.querySelectorAll('.tb-calendar-v2-departure__toggle-options input[type="radio"]');
157            if (options.length === 0) {
158                return;
159            }
160
161            // Keep the first reading: data-price follows the visitor's choice, so re-running
162            // init() must not mistake an already-adjusted price for the base.
163            const basePrice = parseFloat(departureElement.dataset.basePrice ?? departureElement.dataset.price);
164            if (isNaN(basePrice)) {
165                return;
166            }
167            departureElement.dataset.basePrice = String(basePrice);
168
169            const updatePrice = () => {
170                let delta = 0;
171                departureElement
172                    .querySelectorAll('.tb-calendar-v2-departure__toggle-options input[type="radio"]:checked')
173                    .forEach((option) => {
174                        delta += parseFloat(option.dataset.priceDelta) || 0;
175                    });
176
177                const total = parseFloat(departureElement.dataset.basePrice) + delta;
178                departureElement.dataset.price = String(total);
179
180                departureElement
181                    .querySelectorAll('.tb-calendar-v2-departure__price strong, .tb-calendar-v2-departure__big-price strong')
182                    .forEach((priceElement) => {
183                        priceElement.textContent = '€ ' + formatPrice(total);
184                    });
185
186                if (window.tbCurrency) {
187                    window.tbCurrency.processNewContent(departureElement);
188                }
189            };
190
191            options.forEach((option) => {
192                option.addEventListener('change', updatePrice);
193            });
194            // The browser may restore a previous choice on reload; keep the price in step with it.
195            updatePrice();
196        });
197    }
198
199    resizeChildrenOnScreenResize() {
200        window.addEventListener('resize', () => {
201            if (this.departureElements.length > 0) {
202                this.departureElements.forEach((departureElement) => {
203                    if (departureElement.classList.contains('tb-calendar-v2-departure--expanded')) {
204                        departureElement.querySelector('.tb-calendar-v2-departure__more-info').style.height = 'auto';
205                        const height = departureElement.querySelector(
206                            '.tb-calendar-v2-departure__more-info',
207                        ).scrollHeight;
208                        departureElement.querySelector('.tb-calendar-v2-departure__more-info').style.height =
209                            `${height}px`;
210                    }
211                });
212            }
213        });
214    }
215
216    sendViewItemGAEvent(departure) {
217        if (!departure) return;
218        const pkg = departure.dataset.package;
219        tb.track.viewItemInCalendar({
220            id: pkg,
221            name: departure.dataset.packagename,
222            brand: pkg.split('-')[0],
223            variant: departure.dataset.startdate,
224            price: departure.dataset.price,
225        });
226    }
227
228    sendAddToCartGAEvent(event, departure) {
229        if (!departure) return;
230        const pkg = departure.dataset.package;
231        tb.track.addToCart({
232            id: pkg,
233            name: departure.dataset.packagename,
234            brand: pkg.split('-')[0],
235            variant: departure.dataset.startdate,
236            price: departure.dataset.price,
237        });
238    }
239
240    setHiddenInputs() {
241        const brands = this.brands.length != 0 ? Object.values(JSON.parse(this.brands)) : [];
242        const brandNames =
243            Array.isArray(brands) && brands.length !== 0 ? brands.map((brand) => brand.attributes.name) : [];
244        this.keepUpdatedBrand = brandNames.length != 0 ? brandNames[0] : '';
245        this.keepUpdatedBrands = brands;
246        this.keepUpdatedBrandID = brands.length != 0 ? (brands[0] ? brands[0]['id'] : '') : '';
247
248        const packageIds = this.packageIds.length != 0 ? JSON.parse(this.packageIds) : [];
249        const projects = [];
250        const tours = [];
251        packageIds.forEach((packageId) => {
252            const project = packageId.split('-')[0];
253            const tour = packageId.split('-')[1];
254            if (!projects.includes(project)) {
255                projects.push(project);
256            }
257            if (!tours.includes(tour)) {
258                tours.push(tour);
259            }
260        });
261        this.keepUpdatedProject = projects.length != 0 ? projects[0] : '';
262        this.keepUpdatedTour = tours.length != 0 ? tours[0] : '';
263
264        this.handleKeepMeUpdated = function (event) {
265            const target = event.target;
266            if (
267                target.classList.contains('tb-calendar-v2-departure__keep-me-updated') ||
268                target.parentElement.classList.contains('tb-calendar-v2-departure__keep-me-updated')
269            ) {
270                if (this.keepMeUpdatedButtons.length > 0) {
271                    const departureElement = event.target.closest('.tb-calendar-v2-departure');
272                    if (departureElement) {
273                        this.keepUpdatedBrand = departureElement.dataset.brandname;
274                        this.keepUpdatedBrands = JSON.parse(this.brands);
275                        this.keepUpdatedBrandID = departureElement.dataset.brandid;
276                        this.keepUpdatedPackage = departureElement.dataset.package;
277                        this.keepUpdatedProject = this.keepUpdatedPackage.split('-')[0];
278                        this.keepUpdatedTour = this.keepUpdatedPackage.split('-')[1];
279                        this.keepUpdatedDeparture = departureElement.dataset.departure;
280                        this.keepUpdatedDestination = departureElement.dataset.destination;
281                    }
282                }
283            } else {
284                if (this.generalKeepMeUpdatedButton) {
285                    const brands = Object.values(JSON.parse(this.brands));
286                    // this.keepUpdatedBrand = brands[0]['attributes']['name'];
287                    // this.keepUpdatedBrandID = brands[0]['id'];
288                    // if (this.brands.length == 1) {
289                    //     this.keepUpdatedBrand = JSON.parse(this.brands)[0]['attributes']['name'];
290                    // }
291                    this.keepUpdatedBrands = JSON.parse(this.brands);
292                }
293            }
294            this.updateHiddenInputs();
295        }.bind(this);
296
297        this.keepMeUpdatedButtons.forEach((keepMeUpdatedButton) => {
298            keepMeUpdatedButton.removeEventListener('click', this.handleKeepMeUpdated);
299            keepMeUpdatedButton.addEventListener('click', this.handleKeepMeUpdated);
300        });
301
302        if (this.generalKeepMeUpdatedButton) {
303            this.generalKeepMeUpdatedButton.removeEventListener('click', this.handleKeepMeUpdated);
304            this.generalKeepMeUpdatedButton.addEventListener('click', this.handleKeepMeUpdated);
305        }
306
307        this.updateHiddenInputs();
308    }
309
310    updateHiddenInputs() {
311        if (this.hiddenInputs.length > 0) {
312            this.hiddenInputs.forEach((hiddenInput) => {
313                let hiddenInputName = hiddenInput.name;
314                if (hiddenInputName.includes('brand')) {
315                    if (this.keepUpdatedBrandID) {
316                        hiddenInput.value = this.keepUpdatedBrandID;
317                    }
318                }
319                if (hiddenInputName.includes('brands')) {
320                    if (this.keepUpdatedBrands) {
321                        hiddenInput.value = JSON.stringify(this.keepUpdatedBrands);
322                    }
323                }
324                if (hiddenInputName.includes('project')) {
325                    if (this.keepUpdatedProject) {
326                        hiddenInput.value = this.keepUpdatedProject;
327                    }
328                }
329                if (hiddenInputName.includes('tour')) {
330                    if (this.keepUpdatedTour) {
331                        hiddenInput.value = this.keepUpdatedTour;
332                    }
333                }
334                if (hiddenInputName.includes('departure')) {
335                    if (this.keepUpdatedDeparture) {
336                        hiddenInput.value = this.keepUpdatedDeparture;
337                    }
338                }
339                if (hiddenInputName.includes('destination')) {
340                    if (this.keepUpdatedDestination) {
341                        hiddenInput.value = this.keepUpdatedDestination;
342                    }
343                }
344            });
345        }
346    }
347
348    setActiveMonthOnInput() {
349        this.sectionTogglers.forEach((sectionToggler) => {
350            const sectionTogglerInputs = sectionToggler.querySelectorAll('input');
351            sectionTogglerInputs.forEach((input) => {
352                input.removeEventListener('click', this.handleInputClick);
353
354                const handleInputClick = () => {
355                    this.getActiveYearAndMonth();
356                    this.getMaxYearAndMonth();
357                };
358
359                input.addEventListener('click', handleInputClick);
360            });
361        });
362    }
363
364    getActiveYearAndMonth() {
365        if (this.sectionTogglers.length !== 0) {
366            this.sectionTogglers.forEach((sectionToggler) => {
367                const sectionTogglerInputs = sectionToggler.querySelectorAll('input');
368                sectionTogglerInputs.forEach((input) => {
369                    if (input.checked) {
370                        if (input.value.includes('month')) {
371                            // this.activeYear = input.value.split('-')[1];
372                            const yearNumber = parseInt(input.value.split('-')[1]);
373                            if (yearNumber === this.activeYear) {
374                                const monthNumber = parseInt(input.value.split('-')[4]);
375                                this.activeMonth = monthNumber;
376                            }
377                        } else {
378                            if (input.value.includes('year')) {
379                                this.activeYear = parseInt(input.value.split('-')[2]);
380                            }
381                        }
382                    }
383                });
384            });
385        }
386    }
387
388    getMaxYearAndMonth() {
389        this.allYears = [];
390        this.allMonths = [];
391        this.maxYear = 0;
392        this.maxMonth = 0;
393        this.sectionTogglers.forEach((sectionToggler) => {
394            const sectionTogglerInputs = sectionToggler.querySelectorAll('input:not(.inactive)');
395            sectionTogglerInputs.forEach((input) => {
396                if (input.value.includes('month')) {
397                    const yearNumber = parseInt(input.value.split('-')[1]);
398                    if (yearNumber === this.activeYear) {
399                        const monthNumber = parseInt(input.value.split('-')[4]);
400                        this.allMonths.push(monthNumber);
401                    }
402                } else {
403                    if (input.value.includes('year')) {
404                        const yearNumber = parseInt(input.value.split('-')[2]);
405                        this.allYears.push(yearNumber);
406                    }
407                }
408            });
409        });
410        if (this.allYears.length !== 0) {
411            this.allYears.forEach((year) => {
412                if (year > this.maxYear) {
413                    this.maxYear = parseInt(year);
414                }
415                if (year < this.minYear) {
416                    this.minYear = parseInt(year);
417                }
418            });
419        }
420        if (this.allMonths.length !== 0) {
421            this.allMonths.forEach((month) => {
422                if (month > this.maxMonth) {
423                    this.maxMonth = parseInt(month);
424                }
425                if (month < this.minMonth) {
426                    this.minMonth = parseInt(month);
427                }
428            });
429        }
430    }
431
432    enableJs() {
433        if (this.sectionTogglers.length !== 0) {
434            this.sectionTogglers.forEach((sectionToggler) => {
435                sectionToggler.classList.add('section-toggler--enabled');
436            });
437        }
438        if (this.expansionToggles.length !== 0) {
439            this.expansionToggles.forEach((expansionToggle) => {
440                expansionToggle.classList.add('tb-calendar-v2-departure__expand-button--enabled');
441            });
442        }
443
444        if (this.filters.length !== 0) {
445            this.filters.forEach((filter) => {
446                filter.classList.add('tb-calendar-v2__filter--enabled');
447            });
448        }
449
450        if (this.showPrevButtons.length > 0) {
451            this.showPrevButtons.forEach((button) => {
452                button.removeEventListener('click', this.boundLoadMonth);
453                button.addEventListener('click', this.boundLoadMonth);
454            });
455        }
456
457        if (this.showNextButtons.length > 0) {
458            this.showNextButtons.forEach((button) => {
459                button.removeEventListener('click', this.boundLoadMonth);
460                button.addEventListener('click', this.boundLoadMonth);
461            });
462        }
463    }
464
465    initDepartureExpansionBoxes() {
466        this.departureElements.forEach((departureElement) => {
467            if (
468                departureElement.dataset.availability !== 'sold-out' ||
469                departureElement.classList.contains('tb-calendar-v2-departure--sold-out-expandable')
470            ) {
471                // The whole card toggles, so a tap next to "More info" on mobile still opens it.
472                departureElement.addEventListener('click', this.clickedDeparture.bind(this));
473            }
474
475            const participantsButtons = departureElement.querySelectorAll(
476                '.tb-calendar-v2-departure__participants-button',
477            );
478            if (participantsButtons) {
479                participantsButtons.forEach((participantsButton) => {
480                    participantsButton.removeEventListener('click', this.clickedParticipantsButton.bind(this));
481                    participantsButton.addEventListener('click', this.clickedParticipantsButton.bind(this));
482                });
483            }
484        });
485    }
486
487    isParticipantsContainerEmpty(participantsContainer) {
488        if (participantsContainer) {
489            return (
490                participantsContainer.innerHTML.trim() === '' ||
491                participantsContainer.querySelector('.tb-ajax-loader') !== null
492            );
493        }
494    }
495
496    clickedDeparture(event) {
497        const departureElement = event.currentTarget.closest('.tb-calendar-v2__departure');
498        const participantsContainer = departureElement.querySelector(
499            '.tb-calendar-v2-departure__participants-container',
500        );
501
502        // The open panel holds its own controls (options, CTAs, a sold-out group's rows).
503        if (event.target.closest('.tb-calendar-v2-departure__more-info')) {
504            return;
505        }
506
507        if (
508            event.target.tagName.toLowerCase() === 'button' ||
509            event.target.tagName.toLowerCase() === 'a' ||
510            event.target.parentElement.tagName.toLowerCase() === 'button' ||
511            event.target.parentElement.tagName.toLowerCase() === 'a'
512        ) {
513            return;
514        }
515
516        if (departureElement.classList.contains('tb-calendar-v2-departure--expanded')) {
517            departureElement.classList.remove('tb-calendar-v2-departure--expanded');
518            departureElement.querySelector('.tb-calendar-v2-departure__more-info').style.height = 0;
519        } else {
520            departureElement.classList.add('tb-calendar-v2-departure--expanded');
521            departureElement.querySelector('.tb-calendar-v2-departure__more-info').style.height = 0;
522            const height = departureElement.querySelector('.tb-calendar-v2-departure__more-info').scrollHeight;
523            departureElement.querySelector('.tb-calendar-v2-departure__more-info').style.height = `${height}px`;
524            this.expandedDeparture = departureElement;
525            
526            this.sendViewItemGAEvent(this.expandedDeparture);
527
528            const bookNowButton = departureElement.querySelector('.tb-button-book-now');
529            if (bookNowButton) {
530                const boundHandler = (event) => this.sendAddToCartGAEvent(event, departureElement);
531                bookNowButton.removeEventListener('click', boundHandler);
532                bookNowButton.addEventListener('click', boundHandler);
533            }
534
535            if (participantsContainer && this.showParticipants) {
536                if (this.isParticipantsContainerEmpty(participantsContainer)) {
537                    this.updateParticipants(departureElement, false);
538                }
539            }
540        }
541
542        if (this.showParticipants) {
543            this.updateParticipants(departureElement);
544        }
545    }
546    clickedParticipantsButton(event) {
547        const departureElement = event.currentTarget.closest('.tb-calendar-v2__departure');
548        const participantsContainer = departureElement.querySelector(
549            '.tb-calendar-v2-departure__participants-container',
550        );
551
552        // Show the popup with loader first
553        this.showParticipantsPopup('<div class="tb-ajax-loader"></div>');
554
555        // Load the data
556        this.updateParticipants(departureElement, true);
557    }
558
559    updateParticipants(departureElement, showPopup = false) {
560        const departureId = departureElement.getAttribute('data-departure');
561        const participantsContainers = departureElement.querySelectorAll(
562            '.tb-calendar-v2-departure__participants-container',
563        );
564        const data = {
565            action: 'reload_participants_list',
566            departureId: departureId,
567        };
568
569        if (this.participantsXhr && this.participantsXhr.readyState !== 4) {
570            this.participantsXhr.abort();
571        }
572
573        this.participantsXhr.open('POST', this.ajaxUrl, true);
574        this.participantsXhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
575
576        this.participantsXhr.onreadystatechange = () => {
577            if (this.participantsXhr.readyState === 4) {
578                if (this.participantsXhr.status === 200) {
579                    try {
580                        const response = JSON.parse(this.participantsXhr.responseText);
581                        for (let i = 0; i < participantsContainers.length; i++) {
582                            if (response.data.html === '') {
583                                participantsContainers[i].innerHTML = this.noParticipantsText;
584                            } else {
585                                participantsContainers[i].innerHTML = response.data.html;
586                            }
587                        }
588                        if (showPopup) {
589                            if (response.data.html === '') {
590                                this.showParticipantsPopup(
591                                    '<h5>' +
592                                        this.noParticipantsTitle +
593                                        "</h5><div class='wysiwyg'>" +
594                                        this.noParticipantsText +
595                                        '</div>',
596                                );
597                            } else {
598                                this.showParticipantsPopup(response.data.html);
599                            }
600                        }
601                        // reanimate height of departure element
602                        if (departureElement.classList.contains('tb-calendar-v2-departure--expanded')) {
603                            const height = departureElement.querySelector(
604                                '.tb-calendar-v2-departure__more-info',
605                            ).scrollHeight;
606                            departureElement.querySelector('.tb-calendar-v2-departure__more-info').style.height =
607                                `${height}px`;
608                        }
609                    } catch (error) {
610                        console.error('Error parsing response:', error);
611                    }
612                } else {
613                    // console.error(`Error: ${this.participantsXhr.status} - ${this.participantsXhr.statusText}`);
614                }
615            }
616        };
617
618        this.participantsXhr.send(new URLSearchParams(data).toString());
619    }
620
621    showParticipantsPopup(html) {
622        let dialog = document.querySelector('.tb-popup--participants-wrapper');
623        let participantsList;
624
625        if (!dialog) {
626            dialog = document.createElement('dialog');
627            dialog.classList.add('tb-popup--participants-wrapper', 'tb-popup', 'tb-popup-with-padding');
628
629            const dialogContent = document.createElement('div');
630            dialogContent.classList.add('tb-popup-inner', 'tb-block');
631            dialog.appendChild(dialogContent);
632
633            const popup = document.createElement('div');
634            popup.classList.add('tb-popup', 'tb-popup--participants');
635            dialogContent.appendChild(popup);
636
637            participantsList = document.createElement('div');
638            participantsList.classList.add('tb-calendar-v2-departure__participants-container');
639            participantsList.innerHTML = '<div class="tb-ajax-loader"></div>'; // Show loader initially
640            popup.appendChild(participantsList);
641
642            const dialogClose = document.createElement('div');
643            dialogClose.classList.add('tb-popup-close');
644            dialogClose.innerHTML = '<i class="fa-solid fa-xmark fa-fw"></i>';
645            dialogContent.appendChild(dialogClose);
646
647            dialogClose.addEventListener('click', () => {
648                dialog.close();
649            });
650
651            document.body.appendChild(dialog); // Append dialog to the document body
652            dialog.showModal();
653        } else {
654            participantsList = dialog.querySelector('.tb-calendar-v2-departure__participants-container');
655            participantsList.innerHTML = '<div class="tb-ajax-loader"></div>'; // Show loader initially
656            dialog.showModal();
657        }
658
659        // Animate height change after loading the content
660        const originalHeight = participantsList.scrollHeight;
661        participantsList.style.height = `${originalHeight}px`;
662        participantsList.style.overflow = 'hidden';
663        participantsList.style.transition = 'height 0.3s ease-out';
664
665        // Force a reflow to ensure the transition is applied
666        participantsList.getBoundingClientRect();
667
668        // Set the new content and animate the height
669        setTimeout(() => {
670            participantsList.innerHTML = html;
671            participantsList.style.height = 'auto';
672            const newHeight = participantsList.scrollHeight;
673            participantsList.style.height = `${originalHeight}px`;
674
675            requestAnimationFrame(() => {
676                participantsList.style.height = `${newHeight}px`;
677            });
678
679            participantsList.addEventListener(
680                'transitionend',
681                () => {
682                    participantsList.style.overflow = '';
683                    participantsList.style.transition = '';
684                },
685                { once: true },
686            );
687        }, 50); // Slight delay to ensure the initial height is calculated correctly
688    }
689
690    loadMonth(event) {
691        let direction = 'next';
692        if (Array.from(this.showPrevButtons).includes(event.currentTarget)) {
693            direction = 'prev';
694        }
695
696        let monthIndexInArray = this.allMonths.indexOf(this.activeMonth);
697        let yearIndexInArray = this.allYears.indexOf(this.activeYear);
698
699        let nextMonthIndexInArray = monthIndexInArray + 1;
700        let nextYearIndexInArray = yearIndexInArray + 1;
701
702        if (nextMonthIndexInArray >= this.allMonths.length) {
703            nextMonthIndexInArray = 0;
704        }
705        if (nextYearIndexInArray >= this.allYears.length) {
706            nextYearIndexInArray = 0;
707        }
708
709        let previousMonthIndexInArray = monthIndexInArray - 1;
710        let previousYearIndexInArray = yearIndexInArray - 1;
711
712        if (previousMonthIndexInArray < 0) {
713            previousMonthIndexInArray = this.allMonths.length - 1;
714        }
715        if (previousYearIndexInArray < 0) {
716            previousYearIndexInArray = this.allYears.length - 1;
717        }
718
719        // let targetMonth = 0;
720        let targetYear = this.activeYear;
721        let targetMonth = this.allMonths[nextMonthIndexInArray];
722
723        if (direction === 'next') {
724            targetYear = this.activeYear;
725            targetMonth = this.allMonths[nextMonthIndexInArray];
726            if (targetMonth > this.maxMonth) {
727                if (this.allYears.length > 0) {
728                    targetYear = this.allYears[nextYearIndexInArray];
729                    if (targetYear > this.maxYear) {
730                        targetYear = this.minYear;
731                    }
732                }
733                targetMonth = this.minMonth;
734            }
735        } else {
736            targetYear = this.activeYear;
737            targetMonth = this.allMonths[previousMonthIndexInArray];
738            if (targetMonth < this.minMonth) {
739                if (this.allYears.length > 0) {
740                    targetYear = this.allYears[previousYearIndexInArray];
741                    if (targetYear < this.minYear) {
742                        targetYear = this.maxYear;
743                    }
744                }
745                targetMonth = this.maxMonth;
746            }
747        }
748
749        this.forceMonthChange(targetMonth, targetYear);
750
751        setTimeout(() => {
752            const targetElement = this.element.querySelector('.tb-block');
753            const offset = -100;
754            const bodyRect = document.body.getBoundingClientRect().top;
755            const elementRect = targetElement.getBoundingClientRect().top;
756            const elementPosition = elementRect - bodyRect;
757            const offsetPosition = elementPosition + offset;
758
759            window.scrollTo({
760                top: offsetPosition,
761                behavior: 'smooth',
762            });
763        }, 200);
764    }
765
766    forceMonthChange(targetMonth, targetYear) {
767        this.sectionTogglers.forEach((sectionToggler) => {
768            const sectionTogglerInputs = sectionToggler.querySelectorAll('input:not(.inactive)');
769
770            sectionTogglerInputs.forEach((input) => {
771                if (input.value.includes('month')) {
772                    if (input.value.split('-')[4] == targetMonth && input.value.split('-')[1] == targetYear) {
773                        input.checked = true;
774                        const event = new CustomEvent('click', {
775                            detail: { type: 'toggleClick', targetId: input.value },
776                        });
777                        sectionToggler.dispatchEvent(event);
778                    }
779                } else {
780                    if (input.value.includes('year')) {
781                        if (input.value.split('-')[2] == targetYear) {
782                            input.checked = true;
783                            const event = new CustomEvent('click', {
784                                detail: { type: 'toggleClick', targetId: input.value },
785                            });
786                            sectionToggler.dispatchEvent(event);
787                        }
788                    }
789                }
790            });
791        });
792        this.getMaxYearAndMonth();
793        this.getActiveYearAndMonth();
794
795        this.activeMonth = targetMonth;
796        this.activeYear = targetYear;
797    }
798    setFilters() {
799        if (this.filters.length !== 0) {
800            this.filters.forEach((filter) => {
801                const selectElement = filter.querySelector('select');
802                
803                selectElement.removeEventListener('change', this.handleFilterChange);
804
805                // Brand and destination only: booking options are picked per departure.
806                const setFilterValues = () => {
807                    const selectedType = selectElement.getAttribute('data-type');
808                    const selectedValue = selectElement.value;
809                    const filterIndex = this.activeFilters.findIndex((f) => f.type === selectedType);
810
811                    if (selectedValue === '') {
812                        if (filterIndex !== -1) {
813                            this.activeFilters.splice(filterIndex, 1);
814                        }
815                    } else if (filterIndex !== -1) {
816                        this.activeFilters[filterIndex].value = selectedValue;
817                    } else {
818                        this.activeFilters.push({ type: selectedType, value: selectedValue });
819                    }
820                };
821
822                setFilterValues();
823                
824                const handleFilterChange = () => {
825                    const previousFilters = JSON.stringify(this.activeFilters);
826
827                    setFilterValues();
828                    const currentFilters = JSON.stringify(this.activeFilters);
829                    
830                    if (previousFilters !== currentFilters) {
831                        this.loadNewContent();
832                    }
833                };
834
835                selectElement.addEventListener('change', handleFilterChange);
836            });
837        }
838
839        return this.activeFilters;
840    }
841
842    loadNewContent() {
843        if (this.xhr && this.xhr.readyState !== 4) {
844            this.xhr.abort();
845        }
846
847        if (this.skeleton) {
848            this.skeleton.classList.remove('tb-skeleton--inactive');
849        }
850
851        const data = {
852            action: 'soft_reload_inner_calendar',
853            year: this.activeYear,
854            month: this.activeMonth,
855            filters: JSON.stringify(this.activeFilters),
856            packageIds: this.packageIds,
857            fieldPackages: this.fieldPackages,
858            calendarData: this.calendarData,
859            allowCollapseConsecutiveSoldOutItems: this.allowCollapseConsecutiveSoldOutItems,
860            showPreregItems: this.showPreregItems,
861            showParticipants: this.showParticipants,
862            showDepartureHours: this.showDepartureHours,
863            showMonthsToggle: this.showMonthsToggle,
864            showWaitingListButtons: this.showWaitingListButtons,
865            summaryContent: this.summaryContent,
866            summaryInclusive: this.summaryInclusive,
867            soldOutCta: this.soldOutCta,
868            showQuoteRequest: this.showQuoteRequest,
869            sortFilterOptionsByAZOrder: this.sortFilterOptionsByAZOrder,
870            showPriceToggle: this.showPriceToggle,
871            mergeDuplicateFilters: this.mergeDuplicateFilters,
872        };
873
874        this.xhr.open('POST', this.ajaxUrl, true);
875        this.xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
876
877        this.xhr.onreadystatechange = function () {
878            if (this.xhr.readyState === 4) {
879                if (this.xhr.status === 200) {
880                    const response = JSON.parse(this.xhr.responseText);
881                    this.element.innerHTML = response.data.html;
882                    this.init();
883                    // Trigger currency conversion after new content is loaded
884                    if (window.tbCurrency) {
885                        window.tbCurrency.processNewContent(this.element);
886                    }
887                    // initCustomSelects();
888                    // initSectionTogglers();
889                } else if (this.xhr.status === 500) {
890                    console.error('Error:', this.xhr.statusText);
891                }
892                if (this.skeleton) {
893                    this.skeleton.classList.add('tb-skeleton--inactive');
894                }
895            }
896        }.bind(this);
897
898        this.xhr.onerror = function () {
899            console.error('Request failed');
900        };
901
902        this.xhr.send(this.encodeFormData(data));
903    }
904
905    loadInitialContent(initialActiveFilters = []) {
906        if (this.xhr && this.xhr.readyState !== 4) {
907            this.xhr.abort();
908        }
909
910        if (this.skeleton) {
911            this.skeleton.classList.remove('tb-skeleton--inactive');
912        }
913
914        const data = {
915            action: 'reload_inner_calendar',
916            year: this.activeYear,
917            month: this.activeMonth,
918            filters: JSON.stringify(initialActiveFilters),
919            packageIds: this.packageIds,
920            fieldPackages: this.fieldPackages,
921            allowCollapseConsecutiveSoldOutItems: this.allowCollapseConsecutiveSoldOutItems,
922            showPreregItems: this.showPreregItems,
923            showParticipants: this.showParticipants,
924            showDepartureHours: this.showDepartureHours,
925            showMonthsToggle: this.showMonthsToggle,
926            showWaitingListButtons: this.showWaitingListButtons,
927            calendarData: this.calendarData,
928            summaryContent: this.summaryContent,
929            summaryInclusive: this.summaryInclusive,
930            soldOutCta: this.soldOutCta,
931            defaultGroupSize: this.defaultGroupSize,
932            showQuoteRequest: this.showQuoteRequest,
933            sortFilterOptionsByAZOrder: this.sortFilterOptionsByAZOrder,
934            showPriceToggle: this.showPriceToggle,
935            mergeDuplicateFilters: this.mergeDuplicateFilters,
936        };
937
938        this.xhr.open('POST', this.ajaxUrl, true);
939        this.xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
940
941        this.xhr.onreadystatechange = function () {
942            if (this.xhr.readyState === 4) {
943                if (this.xhr.status === 200) {
944                    const response = JSON.parse(this.xhr.responseText);
945                    this.element.innerHTML = response.data.html;
946
947                    this.init();
948                    // Trigger currency conversion after new content is loaded
949                    if (window.tbCurrency) {
950                        window.tbCurrency.processNewContent(this.element);
951                    }
952                    // initCustomSelects();
953                    // initSectionTogglers();
954                } else {
955                    console.error('Error:', this.xhr.statusText);
956                }
957                if (this.skeleton) {
958                    this.skeleton.classList.add('tb-skeleton--inactive');
959                }
960            }
961        }.bind(this);
962
963        this.xhr.onerror = function () {
964            console.error('Request failed');
965        };
966
967        this.xhr.send(this.encodeFormData(data));
968    }
969
970    encodeFormData(data) {
971        return Object.keys(data)
972            .map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key]))
973            .join('&');
974    }
975}

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.