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.