1const header = document.querySelector("header"); 2const heroSection = document.querySelector(".hero"); 3const offset = 95; 4 5window.addEventListener("scroll", () => { 6 // Get the height of the hero section 7 const heroHeight = heroSection.offsetHeight; 8 9 // Check the scroll position 10 if (window.scrollY > heroHeight - offset) { 11 header.classList.add("scrolled"); 12 } else { 13 header.classList.remove("scrolled"); 14 } 15}); 16// }); 17 18// CURRENCY MENU 19 20const buttonCurrency = document.querySelector('.currency-toggle'); // Select the button 21const menuCurrency = document.querySelector('div.currency'); // Select the mobile navigation 22const mobileButtonCurrency = document.querySelector('.currency-selector'); // Select the mobile button 23const mobileMenuCurrency = document.querySelector('.currency-menu'); 24const currency = document.getElementById('input-currency'); 25if (buttonCurrency != null) { 26 buttonCurrency.addEventListener('click', () => { 27 const isExpanded = buttonCurrency.getAttribute('aria-expanded') === 'true'; 28 29 // Toggle aria-expanded attribute 30 buttonCurrency.setAttribute('aria-expanded', !isExpanded); 31 32 // Toggle the 'hidden' class 33 menuCurrency.classList.toggle('hidden'); 34 35 if (currency !== null) { 36 currency.setAttribute('aria-expanded', 'false'); 37 } 38 }); 39} 40 41if (currency !== null) { 42 currency.addEventListener('click', (e) => { 43 e.preventDefault(); 44 const isExpanded = currency.getAttribute('aria-expanded') === 'true'; 45 currency.setAttribute('aria-expanded', !isExpanded); 46 menuCurrency.classList.toggle('hidden'); 47 }); 48} 49 50if (mobileButtonCurrency !== null) { 51 mobileButtonCurrency.addEventListener('click', () => { 52 const isExpanded = mobileButtonCurrency.getAttribute('aria-expanded') === 'true'; 53 // Toggle aria-expanded attribute 54 mobileButtonCurrency.setAttribute('aria-expanded', !isExpanded); 55 // Toggle the 'hidden' class 56 mobileMenuCurrency.classList.toggle('hidden'); 57 }); 58} 59 60// Close the menu if clicking outside 61document.addEventListener('click', (e) => { 62 if (!menuCurrency.parentNode.contains(e.target) && e.target !== currency) { 63 buttonCurrency.setAttribute('aria-expanded', 'false'); 64 if (currency !== null) { 65 currency.setAttribute('aria-expanded', 'false'); 66 } 67 menuCurrency.classList.add('hidden'); 68 } 69}); 70 71$('.currency-item').on('click', function() {
72 let currencyCode = $(this).data('currency'); 73 $('.currency-item').removeClass('active'); 74 $(this).addClass('active'); 75 const url = $(this).parent().data('url'); 76 $.get(url, { 77 currency: currencyCode 78 }) 79 .done(function (data) { 80 if (data.result === 'OK') { 81 if ($('#input-currency').attr('aria-expanded') === 'true') { 82 if (typeof saveFormData === 'function') { 83 saveFormData(); 84 } 85 sessionStorage.setItem(FORM_STEP_STORAGE_KEY, $('.form.current').attr('id')); 86 } 87 removeFromLocalStorage('customTourFormData', ['max-budget', 'max-range', 'min-budget', 'min-range', 'currency-symbol']); 88 location.reload(); 89 } 90 }) 91 .fail(function (xhr) { 92 alert('Currency switch failed.'); 93 console.error(xhr.responseText); 94 }); 95}); 96 97 98function removeFromLocalStorage(storageKey, keysToRemove) { 99 const data = JSON.parse(localStorage.getItem(storageKey) || '{}'); 100 101 keysToRemove.forEach(key => delete data[key]); 102 103 localStorage.setItem(storageKey, JSON.stringify(data)); 104} 105 106 107// MOBILE MENU 108 109const button = document.querySelector('.nav-toggle'); // Select the button 110const navMobile = document.querySelector('nav.mobile'); // Select the mobile navigation 111const overlay = document.querySelector('.overlay'); 112const mobileMenuHeader = document.querySelector('.page-header'); 113const termsAndConditionsLink = document.querySelector('section.important > .terms > .cta'); 114 115button.addEventListener('click', () => { 116 const isExpanded = button.getAttribute('aria-expanded') === 'true'; 117 118 // Toggle aria-expanded attribute 119 button.setAttribute('aria-expanded', !isExpanded); 120 button.classList.toggle('menu-open'); 121 mobileMenuHeader.classList.toggle('menu-open'); 122 123 124 // Toggle the 'hidden' class 125 if (termsAndConditionsLink !== null) { 126 termsAndConditionsLink.classList.toggle('hidden'); 127 } 128 navMobile.classList.toggle('hidden'); 129 overlay.classList.toggle('hidden'); 130}); 131 132 133// FORM DROPDOWN 134 135// Select all dropdown buttons 136const toggleButtons = document.querySelectorAll('.dropdown-toggle'); 137 138toggleButtons.forEach(button => { 139 button.addEventListener('click', () => { 140 // Find the direct parent with the "btn-group" class 141 const dropdownContainer = button.closest('.btn-group'); 142 143 // Toggle the "open" class on the parent div 144 dropdownContainer.classList.toggle('open'); 145 146 if (dropdownContainer.classList.contains('open')) { 147 requestAnimationFrame(() => { 148 dropdownContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); 149 }); 150 } 151 }); 152}); 153 154// Add document click handler to close dropdown when clicking outside 155$(document).on('click', function(e) { 156 // If the click is outside the btn-group 157 if (!$(e.target).closest('.btn-group').length) { 158 // Close all open dropdowns except nav-btn-group 159 $('.btn-group.open:not(.nav-btn-group)').removeClass('open'); 160 } 161}); 162 163// FORM ACTIVE BUTTON Toggle 164$(document).ready(function() { 165 // Define button groups, their input IDs, and selection mode 166 const buttonGroups = [ 167 { selector: '.accommodation-choices', inputId: 'accommodation-input', multiple: true }, 168 { selector: '.transport-type', inputId: 'transport-input', multiple: true }, 169 { selector: '.guide-choice', inputId: 'guide-input', multiple: false } // Single selection 170 ]; 171 172 // Handle all button click events 173 $('.blocks .block').click(function() { 174 // Find which group this button belongs to 175 for (const group of buttonGroups) { 176 if ($(this).hasClass(group.selector.substring(1))) { 177 // If single selection mode, remove active class from other buttons 178 if (!group.multiple) { 179 $(group.selector).removeClass('active'); 180 } 181 182 // Toggle or add active class 183 if (group.multiple) { 184 $(this).toggleClass('active'); 185 } else { 186 $(this).addClass('active'); 187 } 188 189 // Get all selected values from this group 190 const selectedValues = []; 191 $(group.selector + '.active').each(function() { 192 selectedValues.push($(this).data('type')); 193 }); 194 195 // Update the corresponding hidden input 196 $('#' + group.inputId).val(selectedValues.join(',')); 197 break; 198 } 199 } 200 }); 201}); 202 203function showError(element, message) { 204 // Remove any existing error messages 205 element.find('.error-message').remove(); 206 // Add new error message 207 element.append('<div class="error-message" style="color: red; margin-top: 5px; font-size: .9rem">' + message + '</div>'); 208} 209 210function validateCurrentSection(section) { 211 let isValid = true; 212 213 // Validate date input in section 1 214 if (section.id === 'custom-part1') { 215 // Date validation - the hidden field carries an instance-specific id, so match on name 216 const $dateValue = $(section).find('.date-picker-container input[name="tour_date"]'); 217 const $dateContainer = $dateValue.closest('.date-picker-container'); 218 219 if (!$dateValue.length) { 220 console.warn('Tour date field not found in', section.id); 221 } 222 223 if ($dateValue.length && $dateValue.val().trim() === '') { 224 isValid = false;
225 showError($dateContainer, 'Please select a date range'); 226 $dateContainer.find('.date-input-wrapper').addClass('error'); 227 } else { 228 $dateContainer.find('.date-input-wrapper').removeClass('error'); 229 $dateContainer.find('.error-message').remove(); 230 } 231 232 // Purpose validation (if exists and required) 233 if ($('#purpose').length && $('#purpose').prop('required')) { 234 if ($('#purpose').val() === '') { 235 isValid = false; 236 showError($('#purpose').closest('.btn-group'), $('.purpose-error').text()); 237 } else { 238 $('#purpose').closest('.btn-group').find('.error-message').remove(); 239 } 240 } 241 } 242 243 $(section).find('input, textarea, select').not('#purpose, [name="tour_date"], .search-input').each(function () { 244 const field = this; 245 const $field = $(field); 246 247 if (!field.willValidate || field.disabled) { 248 return; 249 } 250 251 const $container = $field.closest('.btn-group'); 252 253 if (!field.checkValidity()) { 254 isValid = false; 255 showError($container, field.validationMessage); 256 } else { 257 $container.find('.error-message').remove(); 258 } 259 }); 260 261 return isValid; 262} 263 264// SWITCHING FORM PARTS VIA BUTTON 265const CUSTOM_TOUR_FADE_MS = 300; 266 267function updateBasicsStepSummary() { 268 const dateText = $('#formatted_tour_date').val(); 269 if (dateText) { 270 $('.step-summary__dates').text(dateText); 271 } 272 273 const { adults, children } = PassengerSelector.passengers; 274 let travelersText = `${adults} adult${adults !== 1 ? 's' : ''}`; 275 if (children > 0) { 276 travelersText += `, ${children} child${children !== 1 ? 'ren' : ''}`; 277 } 278 $('.step-summary__travelers').text(travelersText); 279} 280 281function fadeSwitchSection(currentSection, targetSection, headerPad) { 282 currentSection.style.transition = `opacity ${CUSTOM_TOUR_FADE_MS}ms ease`; 283 currentSection.style.opacity = '0'; 284 285 setTimeout(() => { 286 currentSection.classList.remove('current'); 287 currentSection.style.opacity = ''; 288 currentSection.style.transition = ''; 289 290 targetSection.classList.add('current'); 291 toggleSectionVisibility(); 292 updateBasicsStepSummary(); 293 294 const headerOffset = document.querySelector('header').offsetHeight + headerPad; 295 const elementPosition = targetSection.getBoundingClientRect().top; 296 const offsetPosition = elementPosition + window.pageYOffset - headerOffset; 297 // "auto" would still respect the page's `scroll-behavior: smooth` and 298 // animate â "instant" is what actually skips it while the step is invisible. 299 window.scrollTo({ top: offsetPosition, behavior: 'instant' }); 300 301 targetSection.style.transition = 'none'; 302 targetSection.style.opacity = '0'; 303 void targetSection.offsetHeight; 304 requestAnimationFrame(() => { 305 targetSection.style.transition = `opacity ${CUSTOM_TOUR_FADE_MS}ms ease`; 306 targetSection.style.opacity = '1'; 307 }); 308 }, CUSTOM_TOUR_FADE_MS); 309} 310 311document.querySelectorAll(".btn.primary.submit-action").forEach((button) => { 312 button.addEventListener("click", (event) => { 313 event.preventDefault(); // Prevent the default anchor behavior 314 const currentSection = document.querySelector(".form.current"); 315 316 if (validateCurrentSection(currentSection)) { 317 const nextSection = currentSection.nextElementSibling; 318 if (nextSection && nextSection.classList.contains("form")) { 319 if (nextSection.id) { 320 fadeSwitchSection(currentSection, nextSection, 30); 321 } 322 } 323 } 324 // Check and toggle visibility of sections 325 toggleSectionVisibility(); 326 }); 327}); 328 329document.querySelectorAll(".btn.action.back").forEach((button) => { 330 button.addEventListener("click", (event) => { 331 event.preventDefault(); // Prevent the default anchor behavior 332 333 const currentSection = document.querySelector(".form.current"); 334 if (currentSection) { 335 const previousSection = currentSection.previousElementSibling; 336 if (previousSection && previousSection.classList.contains("form")) { 337 if (previousSection.id) { 338 fadeSwitchSection(currentSection, previousSection, 20); 339 } 340 } 341 } 342 // Check and toggle visibility of sections 343 toggleSectionVisibility(); 344 }); 345}); 346 347// SWITCHING FORM PARTS VIA THE STEP TRACKER (SIDEBAR) 348 349document.querySelectorAll(".progress .step-link").forEach((stepLink) => { 350 stepLink.addEventListener("click", () => { 351 const currentSection = document.querySelector(".form.current"); 352 const targetSection = document.getElementById(stepLink.dataset.stepTarget); 353
354 if (!currentSection || !targetSection || targetSection === currentSection) { 355 return; 356 } 357 358 const formSteps = Array.from(document.querySelectorAll(".mf-form > .form")); 359 const currentIndex = formSteps.indexOf(currentSection); 360 const targetIndex = formSteps.indexOf(targetSection); 361 const isMovingForward = targetIndex > currentIndex; 362 363 // Going back is always fine. Going forward only one step at a time â 364 // skipping ahead would leave an unvalidated step marked "complete" 365 // (checkmark + summary) in the sidebar of whatever step it skipped to. 366 if (isMovingForward && (targetIndex !== currentIndex + 1 || !validateCurrentSection(currentSection))) { 367 return; 368 } 369 370 fadeSwitchSection(currentSection, targetSection, 30); 371 toggleSectionVisibility(); 372 }); 373}); 374 375 376// Check if user is at part 2 or onwards 377 378const sectionsToToggle = document.querySelectorAll('.formshow'); // Sections to hide/show 379 380function toggleSectionVisibility() { 381 // Check if any of .part2, .part3, or .part4 has the .current class 382 const currentPart = document.querySelector(".part2.current, .part3.current, .part4.current"); 383 384 if (currentPart) { 385 // If part2, part3, or part4 is active, hide the sections 386 sectionsToToggle.forEach(section => { 387 section.classList.add('formhidden'); 388 section.classList.remove('formshow'); 389 }); 390 } else { 391 // If part1 (or other parts) is active, show the sections 392 sectionsToToggle.forEach(section => { 393 section.classList.remove('formhidden'); 394 section.classList.add('formshow'); 395 }); 396 } 397} 398 399// RETURNING TO A FORM STEP AFTER A RELOAD (currency switch) 400 401const FORM_STEP_STORAGE_KEY = 'currentFormStep'; 402 403function restoreCurrentFormStep() { 404 const stepId = sessionStorage.getItem(FORM_STEP_STORAGE_KEY); 405 406 // Consumed once: a later visit to the form starts at step one again. 407 sessionStorage.removeItem(FORM_STEP_STORAGE_KEY); 408 409 if (!stepId) { 410 return; 411 } 412 413 const $target = $('section.form#' + stepId).not('.current'); 414 415 if (!$target.length) { 416 return; 417 } 418 419 $('.form.current').removeClass('current'); 420 $target.addClass('current'); 421 toggleSectionVisibility(); 422 updateBasicsStepSummary(); 423 424 window.scrollTo({ 425 top: $target.offset().top - ($('header').outerHeight() + 30), 426 behavior: 'instant', 427 }); 428} 429 430// Initialize visibility on page load 431window.addEventListener("load", () => { 432 restoreCurrentFormStep(); 433 toggleSectionVisibility(); 434 435 requestAnimationFrame(() => { 436 document.documentElement.classList.remove('restoring-form-step'); 437 }); 438}); 439 440// PACKAGE FORM RADIO MESSAGE 441 442const radioYes = document.getElementById("radioYes"); 443const radioNo = document.getElementById("radioNo"); 444const infoText = document.querySelector(".ifno"); 445 446const toggleInfoText = () => { 447 if (radioNo.checked) { 448 infoText.style.display = "block"; // Show the text if "No" is checked 449 } else { 450 infoText.style.display = "none"; // Hide the text for "Yes" or other choices 451 } 452}; 453 454// Add event listeners to both radio buttons 455// radioYes.addEventListener("change", toggleInfoText); 456// radioNo.addEventListener("change", toggleInfoText); 457 458let Search = { 459 bits: { 460 searchForm: $('form.search'), 461 resultsArea: $('#result-container'), 462 pageInput: $('#form-page'), 463 pageItem: '.pagination .arrows a.page-item', 464 }, 465 utils: { 466 submit: function () { 467 history.pushState(Search.bits.searchForm.serializeArray(), 'Search', '?' + Search.bits.searchForm.serialize()); 468 469 Search.utils.scrollToSectionTop(); 470 471 $.get(Search.bits.searchForm.attr('action') + '?' + Search.bits.searchForm.serialize() + '&ajax=1') 472 .done(function (data) { 473 if (typeof (data) == 'string') { 474 data = JSON.parse(data); 475 } 476 Search.bits.resultsArea.html(data.results); // updating results area 477 478 $(Search.bits.pageItem).click(Search.onPaginationItemClick); // Register pagination items 479 }) 480 .fail(function () {}); 481 return false;
482 }, 483 scrollToSectionTop: function () { 484 $('html,body').animate({ 485 scrollTop: Search.bits.searchForm.offset().top - $('header').height() 486 }, 500); 487 }, 488 resetPageNumber: function () { 489 Search.bits.pageInput.val(1) 490 } 491 }, 492 onPaginationItemClick: function (e) { 493 e.preventDefault(); 494 Search.bits.pageInput.val($(this).data('page')); 495 496 return Search.utils.submit(); 497 }, 498 init: function () { 499 // Auto-submit when changing fields 500 $('#selectCat').on('click', '.dropdown-item', function(e) { 501 e.preventDefault(); 502 $('.dropdown-item').removeClass('active'); 503 $(this).addClass('active'); 504 const categoryId = $(this).data('target'); 505 $('input[name="category"]').val(categoryId); 506 $('.btn-group').toggleClass('open'); 507 Search.utils.resetPageNumber(); 508 return Search.utils.submit(); 509 }); 510 511 // Ajax submit 512 Search.bits.searchForm.submit(function() { return Search.utils.submit(); }); 513 514 // Register pagination items 515 $(Search.bits.pageItem).click(Search.onPaginationItemClick); 516 } 517} 518 519let RoomTypeSelector = { 520 types: { 521 single: 'Single', 522 double: 'Twin', 523 }, 524 totalMaxCount: parseInt($(`#max_room_count-single`).val()) + parseInt($(`#max_room_count-twin`).val()), 525 totalRoomCount: parseInt($(`#room_count_single`).val()) + parseInt($(`#room_count_twin`).val()) * 2, 526 init: function () { 527 this.setupEventListeners(); 528 }, 529 setupEventListeners: function () { 530 $('.room-increment').click(function () { 531 RoomTypeSelector.incrementRoomCount($(this).data('type')); 532 }); 533 $('.room-decrement').click(function () { 534 RoomTypeSelector.decrementRoomCount($(this).data('type')); 535 }); 536 }, 537 calculateRoomIncrementCount: function(countSpan, count, type) { 538 let roomCount = count; 539 if (type === 'Twin') { 540 roomCount = Math.ceil(count * 2); 541 } 542 let newCount = count; 543 let passengerCount = PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']; 544 if (passengerCount > roomCount) { 545 // Handle room type adjustments before incrementing 546 if (type == 'Twin') { 547 $('.twin-room').show(); 548 // When incrementing Twin rooms, decrease Single rooms 549 let singleVal = parseInt($(`#room_count_single`).val()) || 0; 550 if (singleVal > 1) { 551 newCount = count + 1; 552 singleVal -= 2; 553 $(`#room_count_single`).val(singleVal); 554 $(`#single-count`).text(singleVal); 555 $('#single_room_count').text(`x${singleVal}`); 556 if (singleVal == 0) { 557 $(`.single-room`).hide(); 558 } 559 } else { 560 if ($('#single-count').length == 0) { 561 newCount = count + 1; 562 } 563 } 564 } else if (type == 'Single') { 565 $('.single-room').show(); 566 // When incrementing Single rooms, optimize twin rooms when needed 567 let newSingleCount = count + 1; 568 let twinVal = parseInt($(`#room_count_twin`).val()) || 0; 569 RoomTypeSelector.totalRoomCount = newSingleCount + twinVal * 2; 570 // Only optimize when total capacity EXCEEDS passenger count (not equals) 571 if (RoomTypeSelector.totalRoomCount >= passengerCount && twinVal > 0) { 572 // Calculate optimal twin count to match passenger count exactly 573 let optimalTwinCount = Math.max(0, Math.ceil((passengerCount - newSingleCount) / 2)); 574 575 // Only reduce if the optimal count is less than current 576 if (optimalTwinCount < twinVal) { 577 $(`#room_count_twin`).val(optimalTwinCount); 578 $(`#twin-count`).text(optimalTwinCount); 579 $('#twin_room_count').text(`x${optimalTwinCount * 2}`); 580 if (optimalTwinCount === 0) { 581 $('.twin-room').hide(); 582 } 583 RoomTypeSelector.totalRoomCount = newSingleCount + optimalTwinCount * 2; 584 } 585 } 586 newCount = newSingleCount; 587 if (RoomTypeSelector.totalRoomCount < passengerCount || RoomTypeSelector.totalRoomCount > passengerCount) { 588 $('.room-type-alert').show().text(`Please adjust the no. of rooms as per guests!`); 589 $('.room-type-counter').addClass('room-type-alert-border'); 590 $('.btn.primary.action').prop('disabled', true).addClass('disabled'); 591 } else { 592 $('.room-type-alert').hide().text(``); 593 $('.room-type-counter').removeClass('room-type-alert-border'); 594 $('.btn.primary.action').prop('disabled', false).removeClass('disabled'); 595 } 596 } 597 countSpan.text(newCount); 598 $(`#room_count_${type.toLowerCase()}`).val(newCount); 599 600 // Update any additional display elements 601 if ($(`#${type.toLowerCase()}_room_count`).length > 0) { 602 if (type === 'Twin') { 603 $(`#${type.toLowerCase()}_room_count`).text(`x${newCount * 2}`); 604 } else { 605 $(`#${type.toLowerCase()}_room_count`).text(`x${newCount}`); 606 } 607 } 608 609 RoomTypeSelector.calculateRoomPrice(); 610 } 611 }, 612 incrementRoomCount: function (type) { 613 const countSpan = $(`#${type.toLowerCase()}-count`); 614 let count = parseInt(countSpan.text()); 615 616 if ($(`#max_room_count-${type.toLowerCase()}`).length > 0) { 617 const maxCount = parseInt($(`#max_room_count-${type.toLowerCase()}`).val()); 618 619 if (count < maxCount) { 620 RoomTypeSelector.calculateRoomIncrementCount(countSpan, count, type); 621 } else { 622 RoomTypeSelector.twinCountNotice(); 623 } 624 } 625 626 if (parseInt(PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']) > RoomTypeSelector.totalMaxCount) { 627 RoomTypeSelector.maxCountNotice(); 628 } 629 }, 630 decrementRoomCount: function (type) { 631 const countSpan = $(`#${type.toLowerCase()}-count`); 632 let count = parseInt(countSpan.text()); 633 if (count > 0) { 634 if (PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children'] < RoomTypeSelector.totalRoomCount) { 635 let newCount = count - 1; 636 countSpan.text(newCount); 637 $(`#room_count_${type.toLowerCase()}`).val(newCount); 638 RoomTypeSelector.calculateRoomPrice(); 639 RoomTypeSelector.hideAllNotice(); 640 $(`#${type.toLowerCase()}_room_count`).text(`x${newCount}`); 641 RoomTypeSelector.totalRoomCount = parseInt($(`#room_count_single`).val()) + parseInt($(`#room_count_twin`).val()) * 2; 642 if (RoomTypeSelector.totalRoomCount < PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']) { 643 $('.room-type-alert').show().text(`Please adjust the no. of rooms as per guests!`); 644 $('.room-type-counter').addClass('room-type-alert-border'); 645 $('.btn.primary.action').prop('disabled', true).addClass('disabled'); 646 } else { 647 $('.room-type-alert').hide().text(``); 648 $('.room-type-counter').removeClass('room-type-alert-border'); 649 $('.btn.primary.action').prop('disabled', false).removeClass('disabled'); 650 } 651 } 652 } 653 },
654 calculateRoomPrice: function () { 655 let totalRoomPrice = 0; 656 let baseRoomPrice = 0; 657 $('.room-types').each(function () { 658 const roomType = $(this).data('room-type'); 659 const roomCount = parseInt($(`#room_count_${roomType.toLowerCase()}`).val()); 660 const roomPrice = parseInt($(`#${roomType.toLowerCase()}_room_price`).val()); 661 if (!isNaN(roomCount) && roomCount > 0 && !isNaN(roomPrice) && roomPrice > 0) { 662 totalRoomPrice += roomCount * roomPrice; 663 baseRoomPrice += roomCount * parseInt($(`#${roomType.toLowerCase()}_room_base_price`).val()); 664 } 665 }); 666 $('#room_amount_input').val(totalRoomPrice); 667 $('#room_base_amount_input').val(baseRoomPrice.toLocaleString()); 668 PassengerSelector.updateTotalAmount(); 669 }, 670 maxCountNotice: function() { 671 if ($('#custom-enquiry-notice').hasClass('hidden')) { 672 $(`#custom-enquiry-notice`).removeClass('hidden'); 673 } 674 if (!$('#twin-room-notice').hasClass('hidden')) { 675 $('#twin-room-notice').addClass('hidden'); 676 } 677 }, 678 twinCountNotice: function() { 679 if (!$('#custom-enquiry-notice').hasClass('hidden')) { 680 $(`#custom-enquiry-notice`).addClass('hidden'); 681 } 682 if ($('#twin-room-notice').hasClass('hidden')) { 683 $('#twin-room-notice').removeClass('hidden'); 684 } 685 }, 686 hideAllNotice: function() { 687 $('.form-notice span').addClass('hidden'); 688 } 689}; 690 691let PassengerSelector = { 692 passengers: { 693 adults: 1, 694 children: 0, 695 }, 696 updateCounts: function () { 697 // Update individual counters 698 $('.adults-count').text(PassengerSelector.passengers.adults); 699 $('.children-count').text(PassengerSelector.passengers.children); 700 701 // Update summary text 702 let summary = ''; 703 if (PassengerSelector.passengers.adults > 0) { 704 summary = `${PassengerSelector.passengers.adults} Guest${PassengerSelector.passengers.adults !== 1 ? 's' : ''}`; 705 } 706 if (PassengerSelector.passengers.children > 0) { 707 summary = `${PassengerSelector.passengers.adults + PassengerSelector.passengers.children} Guests`; 708 } 709 710 $('.passenger-summary').text(summary); 711 712 // Add hidden inputs for form submission 713 // You can add these inputs to your form data for submission 714 PassengerSelector.updateHiddenInputs(); 715 716 if ($('.enquiry-submit').length > 0) { 717 if (PassengerSelector.passengers.adults + PassengerSelector.passengers.children > 8) { 718 $('.enquiry-submit').addClass('hidden'); 719 $('.enquiry-custom').removeClass('hidden'); 720 } else { 721 $('.enquiry-submit').removeClass('hidden'); 722 $('.enquiry-custom').addClass('hidden'); 723 } 724 } 725 }, 726 727 // Create/update hidden inputs for form submission 728 updateHiddenInputs: function () { 729 // Remove existing inputs 730 $('.passenger-selector input[type="hidden"]').remove(); 731 732 // Add new inputs 733 for (const type in PassengerSelector.passengers) { 734 $('.passenger-selector').append(`<input type="hidden" name="passenger_${type}" value="${PassengerSelector.passengers[type]}">`); 735 } 736 737 if ($('#participants').length > 0) { 738 PassengerSelector.updateTotalAmount(); 739 } 740 741 if ($('#total_people').length > 0) { 742 let totalParticipants = PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']; 743 $('#total_people').text(totalParticipants); 744 } 745 }, 746 updateTotalAmount: function() { 747 let totalParticipants = PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']; 748 $('#participants').text(`x${totalParticipants}`); 749 if ($('#package_price_input').length == 0 || isNaN($('#package_price_input').val())) { 750 return; 751 } 752 let totalAmount = totalParticipants * $('#package_price_input').val(); 753 let baseAmount = totalParticipants * $('#package_base_price_input').val(); 754 755 // Optional tour extension (partner packages): a per-person surcharge added on 756 // top of whichever branch below wins. Zero on every other page, which keeps 757 // both branches identical to their pre-extension behaviour. 758 const extensionPrice = parseInt($('#extension_price_input').val()) || 0; 759 const extensionTotal = (extensionPrice > 0 && $('#extension_selected').val() === '1') 760 ? totalParticipants * extensionPrice 761 : 0; 762 763 if ($('#room_amount_input').val() > 0) { 764 totalAmount = parseInt($('#room_amount_input').val()); 765 const roomBaseAmount = String($('#room_base_amount_input').val()); 766 $('.sale-amount').text(extensionTotal > 0 767 ? ((parseInt(roomBaseAmount.replace(/,/g, '')) || 0) + extensionTotal).toLocaleStr
767ing() 768 : roomBaseAmount); 769 } else { 770 $('.sale-amount').text((baseAmount + extensionTotal).toLocaleString()); 771 totalAmount = parseInt(totalAmount); 772 } 773 totalAmount += extensionTotal; 774 $('.package-widget-price').text(totalAmount.toLocaleString()); 775 $('#total_amount').text(totalAmount.toLocaleString()); 776 $('#total_amount_input').val(totalAmount); 777 }, 778 init: function () { 779 // Toggle dropdown 780 $('.passenger-dropdown-toggle').on('click', function() { 781 const $selector = $(this).closest('.passenger-selector'); 782 $selector.toggleClass('open'); 783 $selector.find('.dropdown-menu').toggle(); 784 }); 785 786 // Close dropdown when clicking outside 787 $(document).on('click', function(e) { 788 if (!$(e.target).closest('.passenger-selector').length) { 789 $('.passenger-selector').removeClass('open'); 790 $('.dropdown-menu').hide(); 791 } 792 }); 793 794 if ($(`#single-count`).length > 0) { 795 RoomTypeSelector.calculateRoomPrice(); 796 } 797 798 799 // Increment passenger count 800 $('.btn-increment').on('click', function() { 801 const type = $(this).data('type'); 802 803 // A blank cap means the departure has no seat limit, same as no cap at all. 804 const maxGuestCount = parseInt($('#max_guest_count').val()); 805 806 if (isNaN(maxGuestCount) || PassengerSelector.getTotalPassengers() < maxGuestCount) { 807 PassengerSelector.passengers[type]++; 808 PassengerSelector.updateCounts(); 809 } 810 811 // Room count update 812 if ($('#single-count').length > 0) { 813 RoomTypeSelector.incrementRoomCount('Single'); 814 } else { 815 if ($('#twin-count').length > 0) { 816 if ($('#twin-count').text() == 0) { 817 $('#twin-count').text(1); 818 } 819 RoomTypeSelector.incrementRoomCount('Twin'); 820 } 821 PassengerSelector.updateTotalAmount(); 822 } 823 }); 824 825 // Decrement passenger count 826 $('.btn-decrement').on('click', function() { 827 const type = $(this).data('type'); 828 if (PassengerSelector.passengers[type] > 0) { 829 PassengerSelector.passengers[type]--; 830 // Ensure at least one adult 831 if (type === 'adults' && PassengerSelector.passengers.adults < 1) { 832 PassengerSelector.passengers.adults = 1; 833 } 834 PassengerSelector.updateCounts(); 835 if ($('#single-count').length > 0) { 836 $('#twin-count').text(0); 837 $(`#room_count_twin`).val(0); 838 $('.twin-room').hide(); 839 let totalCount = parseInt(PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']); 840 $(`#room_count_single`).val(totalCount); 841 $('#single_room_count').text(`x${totalCount}`); 842 $('#single-count').text(totalCount); 843 RoomTypeSelector.calculateRoomPrice(); 844 if (totalCount < $('#single-count').text()) { 845 RoomTypeSelector.decrementRoomCount('Single'); 846 } else { 847 PassengerSelector.updateTotalAmount(); 848 } 849 } else { 850 PassengerSelector.updateTotalAmount(); 851 } 852 } 853 }); 854 855 // Close dropdown on done 856 $('.btn-done').on('click', function() { 857 PassengerSelector.updateCounts(); 858 const $selector = $(this).closest('.passenger-selector'); 859 $selector.removeClass('open'); 860 $selector.find('.dropdown-menu').hide(); 861 }); 862 863 // Update counts in UI and summary 864 PassengerSelector.updateCounts(); 865 }, 866 getTotalPassengers: function() { 867 return PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']; 868 } 869} 870 871let BudgetRangeSlider = { 872 // Settings and state 873 settings: { 874 // Default values 875 minRangeSelector: '#min-range', 876 minBudgetSelector: '#min-budget', 877 sliderProgressSelector: '.slider-progress', 878 totalRangeSelector: '.total-range', 879 // packagePriceSelector: '#package_price', 880 totalDaysSelector: '#total_days', 881 currencySymbol: '#currency-symbol', 882 tripDays: '', 883 inputWrapperSelector: '.currency-input-wrapper', 884 budgetInputFieldSelector: '.budget-input-field', 885 }, 886 887 // Format numbers with commas 888 formatNumber: function(num) { 889 return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 890 }, 891 892 // Parse formatted number strings 893 parseFormattedNumber: function(str) { 894 return parseInt(str.replace(/,/g, '')); 895 }, 896 897 // Get tick values array from data attribute (currency-converted tick boundaries) 898 getTickValues: function() { 899 const $minRangeInput = $(this.settings.minRangeSelector); 900 const raw = $minRangeInput.data('tick-values'); 901 return Array.isArray(raw) ? raw : JSON.parse(raw || '[]'); 902 }, 903 904 // Get the currency's rounding increment for interpolated slider values 905 getStep: function() { 906 const $minRangeInput = $(this.settings.minRangeSelector); 907 const step = parseFloat($minRangeInput.data('currency-step')); 908 return step > 0 ? step : 100; 909 }, 910 911 // Convert continuous slider position (0â4, fractional) to an interpolated currency value 912 // Uses logarithmic interpolation between tick values, rounded to a realistic increment 913 sliderToValue: function(position) { 914 const tickValues = this.getTickValues(); 915 const maxIndex = tickValues.length - 1; 916 const clampedPos = Math.max(0, Math.min(maxIndex, position)); 917 const lowerIndex = Math.floor(clampedPos); 918 const upperIndex = Math.ceil(clampedPos); 919 920 if (lowerIndex === upperIndex) { 921 return tickValues[lowerIndex]; 922 } 923 924 const fraction = clampedPos - lowerIndex; 925 const logLower = Math.log10(tickValues[lowerIndex]); 926 const logUpper = Math.log10(tickValues[upperIndex]); 927 const interpolated = Math.pow(10, logLower + fraction * (logUpper - logLower)); 928
929 const step = this.getStep(); 930 return Math.round(interpolated / step) * step; 931 }, 932 933 // Convert a currency value to the exact fractional slider position (0â4) 934 // Uses logarithmic reverse interpolation for continuous values between ticks 935 valueToSlider: function(value) { 936 const tickValues = this.getTickValues(); 937 const maxIndex = tickValues.length - 1; 938 939 if (value <= tickValues[0]) return 0; 940 if (value >= tickValues[maxIndex]) return maxIndex; 941 942 for (let i = 0; i < maxIndex; i++) { 943 if (value >= tickValues[i] && value <= tickValues[i + 1]) { 944 const logLower = Math.log10(tickValues[i]); 945 const logUpper = Math.log10(tickValues[i + 1]); 946 const logValue = Math.log10(value); 947 return i + (logValue - logLower) / (logUpper - logLower); 948 } 949 } 950 951 return maxIndex; 952 }, 953 954 // Snap slider to the nearest tick index (integer 0â4) 955 snapToNearestTick: function() { 956 const $minRangeInput = $(this.settings.minRangeSelector); 957 const currentPos = parseFloat($minRangeInput.val()); 958 const snappedPos = Math.round(currentPos); 959 $minRangeInput.val(snappedPos); 960 this.isDragging = false; 961 this.updateRangeSlider(); 962 }, 963 964 // Update tick mark visual state for continuous positions 965 // Ticks to the left of the current position are marked passed; none are active 966 updateTickStyles: function(position) { 967 $('.slider-tick').each(function(i) { 968 $(this) 969 .toggleClass('tick-passed', i < position) 970 .removeClass('tick-active'); 971 }); 972 }, 973 974 // Update budget input field width based on content 975 updateInputFieldWidth: function() { 976 const $minBudgetInput = $(this.settings.minBudgetSelector); 977 const $currencyButton = $('#input-currency'); 978 const $currencySymbolEl = $('.currency-symbol-display'); 979 const $budgetInputField = $(this.settings.budgetInputFieldSelector); 980 981 // Skip width adjustment on mobile 982 if ($(window).width() <= 768) { 983 return; 984 } 985 986 if ($minBudgetInput.length > 0 && $budgetInputField.length > 0) { 987 // Create a temporary span to measure the actual text width 988 const $temp = $('<span>') 989 .css({ 990 'position': 'absolute', 991 'visibility': 'hidden', 992 'white-space': 'nowrap', 993 'font-size': $minBudgetInput.css('font-size'), 994 'font-family': $minBudgetInput.css('font-family'), 995 'font-weight': $minBudgetInput.css('font-weight'), 996 'letter-spacing': $minBudgetInput.css('letter-spacing') 997 }) 998 .text($minBudgetInput.val()) 999 .appendTo('body'); 1000 1001 const inputTextWidth = $temp.width() + 10; 1002 $temp.remove(); 1003 1004 const currencyWidth = $currencyButton.length > 0 ? $currencyButton.outerWidth() : 50; 1005 const symbolWidth = $currencySymbolEl.length > 0 ? $currencySymbolEl.outerWidth() : 20; 1006 1007 // content: currency button + gap + symbol + gap + input text 1008 const contentWidth = currencyWidth + 13 + symbolWidth + 8 + inputTextWidth; 1009 1010 // Add padding (16px left + 16px right) 1011 const newWidth = contentWidth + 32; 1012 1013 const minWidth = 220; 1014 const maxWidth = 420; 1015 const finalWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); 1016 1017 $budgetInputField.css('width', finalWidth + 'px'); 1018 } 1019 }, 1020 1021 // Track dragging state 1022 isDragging: false, 1023 1024 // Update the progress bar and display total budget in the input 1025 updateRangeSlider: function() { 1026 const $minRangeInput = $(this.settings.minRangeSelector); 1027 const $minBudgetInput = $(this.settings.minBudgetSelector); 1028 const $sliderProgress = $(this.settings.sliderProgressSelector); 1029 1030 // Continuous position (0â4, fractional during drag) 1031 const sliderPosition = parseFloat($minRangeInput.val()); 1032 const perPersonValue = this.sliderToValue(sliderPosition); 1033 1034 // Show total in the input field 1035 $minBudgetInput.val(this.formatNumber(perPersonValue)); 1036 1037 // Update the budget input field width based on content 1038 this.updateInputFieldWidth(); 1039 1040 // Progress bar spans from 0% to the current position (0â4 â 0â100%) 1041 const progressPercent = (sliderPosition / 4) * 100; 1042 1043 // Disable transition during drag for immediate feedback, enable for snap 1044 if (this.isDragging) { 1045 $sliderProgress.css('transition', 'none'); 1046 } else { 1047 $sliderProgress.css('transition', ''); 1048 } 1049 1050 $sliderProgress.css({ 1051 'left': '0%', 1052 'width': progressPercent + '%' 1053 }); 1054 1055 // Update tick visual states 1056 this.updateTickStyles(sliderPosition); 1057 }, 1058 1059 // Handle input field changes â reverse total to per-person, then snap to nearest tick 1060 handleInputChange: function(event, rangeInputSelector) { 1061 const $rangeInput = $(rangeInputSelector); 1062 const totalValue = this.parseFormattedNumber($(event.target).val()); 1063 const groupSize = Math.max(1, PassengerSelector.passengers['adults'] + PassengerSelector.passengers['children']); 1064 const perPersonValue = Math.round(totalValue / groupSize); 1065 const tickValues = this.getTickValues(); 1066 const minCurrencyVal = tickValues[0]; 1067 1068 if (isNaN(perPersonValue) || perPersonValue < minCurrencyVal) { 1069 $rangeInput.val(0); 1070 } else { 1071 $rangeInput.val(this.valueToSlider(totalValue)); 1072 } 1073 1074 this.updateRangeSlider(); 1075 }, 1076 1077 // Initialize the component 1078 init: function(customSettings) { 1079 // Apply custom settings if provided 1080 if (customSettings) { 1081 $.extend(this.settings, customSettings); 1082 } 1083 1084 const self = this; 1085 const $minRangeInput = $(this.settings.minRangeSelector); 1086 const $minBudgetInput = $(this.settings.minBudgetSelector); 1087 1088 // Set initial slider position from the default currency value via log scale 1089 const initialCurrencyValue = parseFloat($minRangeInput.data('currency-default')); 1090 const initialPosition = self.valueToSlider(initialCurrencyValue); 1091 $minRangeInput.val(initialPosition); 1092 this.updateRangeSlider(); 1093 1094 // Set initial field width 1095 setTimeout(() => { 1096 self.updateInputFieldWidth(); 1097 }, 100); 1098 1099 // Event listeners for range inputs â smooth continuous drag 1100 $minRangeInput.on('input', function() { 1101 self.isDragging = true; 1102 self.updateRangeSlider(); 1103 }); 1104 1105 // Reset dragging state on release without snapping 1106 $minRangeInput.on('change', function() { 1107 self.isDragging = false;
1108 self.updateRangeSlider(); 1109 }); 1110 1111 // Handle mouse enter/leave to control thumb active state 1112 if ($minRangeInput.length > 0) { 1113 const rangeElement = $minRangeInput[0]; 1114 1115 rangeElement.addEventListener('mouseenter', function() { 1116 this.classList.add('slider-hovered'); 1117 }); 1118 1119 rangeElement.addEventListener('mouseleave', function() { 1120 this.classList.remove('slider-hovered'); 1121 }); 1122 } 1123 1124 // Event listeners for text inputs 1125 $minBudgetInput.on('change', function(e) { 1126 self.handleInputChange(e, self.settings.minRangeSelector); 1127 }); 1128 1129 $minBudgetInput.on('keydown', function(e) { 1130 if (e.key === 'Enter' || e.keyCode === 13) { 1131 e.preventDefault(); 1132 1133 // Trigger the existing change event to use your existing functionality 1134 $(this).trigger('change'); 1135 1136 // Remove focus from input 1137 $(this).blur(); 1138 } else { 1139 $(this).trigger('change'); 1140 } 1141 }); 1142 1143 // Format text inputs on blur 1144 $minBudgetInput.on('blur', function() { 1145 $(this).val(self.formatNumber(self.parseFormattedNumber($(this).val()))); 1146 }); 1147 1148 // If the PassengerSelector updates, we should update our budget calculation 1149 $(document).on('passengerCountUpdated', function() { 1150 self.updateRangeSlider(); 1151 }); 1152 } 1153}; 1154 1155// GRID SCROLL ARROWS 1156function initScrollArrows() { 1157 const sections = document.querySelectorAll('.scroll-cards'); 1158 if (!sections.length) return; 1159 1160 sections.forEach(function (section) { 1161 const grid = section.querySelector('.scroll-card--grid'); 1162 const prevBtn = section.querySelector('.scroll-arrow--prev'); 1163 const nextBtn = section.querySelector('.scroll-arrow--next'); 1164 if (!grid || !prevBtn || !nextBtn) return; 1165 1166 function updateArrowState() { 1167 prevBtn.disabled = grid.scrollLeft <= 0; 1168 nextBtn.disabled = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 1; 1169 } 1170 1171 function getScrollAmount() { 1172 const card = grid.querySelector('.tour-card'); 1173 if (card) return card.offsetWidth + 15; // card width + gap 1174 return grid.clientWidth * 0.85; 1175 } 1176 1177 prevBtn.addEventListener('click', function () { 1178 grid.scrollBy({ left: -getScrollAmount(), behavior: 'smooth' }); 1179 }); 1180 1181 nextBtn.addEventListener('click', function () { 1182 grid.scrollBy({ left: getScrollAmount(), behavior: 'smooth' }); 1183 }); 1184 1185 grid.addEventListener('scroll', updateArrowState); 1186 updateArrowState(); 1187 }); 1188} 1189 1190// READ MORE / READ LESS (line-clamped text) 1191 1192let ReadMore = { 1193 init() { 1194 $('[data-readmore]').each(function () { 1195 const $wrap = $(this); 1196 const $text = $wrap.find('.readmore-text'); 1197 const $toggle = $wrap.find('.readmore-toggle'); 1198 if (!$text.length || !$toggle.length) { 1199 return; 1200 } 1201 1202 // Only offer the toggle when the text actually overflows the clamp. 1203 if ($text.prop('scrollHeight') <= $text.prop('clientHeight') + 1) { 1204 $wrap.removeClass('is-collapsed'); 1205 return; 1206 } 1207 1208 $toggle.prop('hidden', false).on('click', function () { 1209 $wrap.toggleClass('is-collapsed'); 1210 $toggle.attr('aria-expanded', String(!$wrap.hasClass('is-collapsed'))); 1211 }); 1212 }); 1213 } 1214}; 1215 1216let SuggestionField = { 1217 closeAllDropdowns: function () { 1218 $('.suggestion-pill-group.open').removeClass('open') 1219 .find('.suggestion-pill--dropdown').attr('aria-expanded', 'false'); 1220 }, 1221 applyValue: function ($field, text) { 1222 const $textarea = $field.find('textarea'); 1223 const mode = $field.data('fill-mode') || 'replace'; 1224 1225 if (mode === 'append') { 1226 const parts = $textarea.val().split(',').map((part) =>
1226 part.trim()).filter(Boolean); 1227 if (!parts.includes(text)) { 1228 parts.push(text); 1229 } 1230 $textarea.val(parts.join(', ')); 1231 } else { 1232 $textarea.val(text); 1233 } 1234 1235 $textarea.trigger('change'); 1236 $textarea.closest('.btn-group').find('.error-message').remove(); 1237 }, 1238 init: function () { 1239 $(document).on('click', '.suggestion-pill:not(.suggestion-pill--dropdown)', function () { 1240 const $pill = $(this); 1241 const $field = $pill.closest('.suggestion-field'); 1242 const text = $pill.text().trim(); 1243 SuggestionField.applyValue($field, text); 1244 }); 1245 1246 $(document).on('click', '.suggestion-pill--dropdown', function (e) { 1247 e.stopPropagation(); 1248 const $group = $(this).closest('.suggestion-pill-group'); 1249 const isOpen = $group.hasClass('open'); 1250 1251 SuggestionField.closeAllDropdowns(); 1252 1253 if (!isOpen) { 1254 $group.addClass('open'); 1255 $(this).attr('aria-expanded', 'true'); 1256 } 1257 }); 1258 1259 $(document).on('click', '.suggestion-dropdown__option', function () { 1260 const $option = $(this); 1261 const $field = $option.closest('.suggestion-field'); 1262 const text = $option.text().trim(); 1263 SuggestionField.applyValue($field, text); 1264 SuggestionField.closeAllDropdowns(); 1265 }); 1266 1267 $(document).on('click', function (e) { 1268 if (!$(e.target).closest('.suggestion-pill-group').length) { 1269 SuggestionField.closeAllDropdowns(); 1270 } 1271 }); 1272 1273 $(document).on('keydown', function (e) { 1274 if (e.key === 'Escape') { 1275 SuggestionField.closeAllDropdowns(); 1276 } 1277 }); 1278 } 1279}; 1280 1281let LanguageTooltip = { 1282 closeAll: function () { 1283 $('.language-tooltip.open').removeClass('open') 1284 .find('.language-tooltip__trigger').attr('aria-expanded', 'false'); 1285 }, 1286 init: function () { 1287 $(document).on('click', '.language-tooltip__trigger', function (e) { 1288 e.stopPropagation(); 1289 const $trigger = $(this); 1290 const $tooltip = $trigger.closest('.language-tooltip'); 1291 const isOpen = $tooltip.hasClass('open'); 1292 1293 LanguageTooltip.closeAll(); 1294 1295 if (!isOpen) { 1296 $tooltip.addClass('open'); 1297 $trigger.attr('aria-expanded', 'true'); 1298 } 1299 }); 1300 1301 $(document).on('click', '.language-tooltip__flag', function (e) { 1302 e.stopPropagation(); 1303 const $flag = $(this); 1304 const lang = $flag.data('lang'); 1305 const $popover = $flag.closest('.language-tooltip__popover'); 1306 1307 $popover.find('.language-tooltip__flag').removeClass('active'); 1308 $flag.addClass('active'); 1309 1310 $popover.find('[data-lang-text]').prop('hidden', true); 1311 $popover.find(`[data-lang-text="${lang}"]`).prop('hidden', false); 1312 }); 1313 1314 $(document).on('click', function (e) { 1315 if (!$(e.target).closest('.language-tooltip').length) { 1316 LanguageTooltip.closeAll(); 1317 } 1318 }); 1319 1320 $(document).on('keydown', function (e) { 1321 if (e.key === 'Escape') { 1322 LanguageTooltip.closeAll(); 1323 } 1324 }); 1325 } 1326}; 1327 1328let PhoneCodeSelector = { 1329 closeAll: function () { 1330 $('.phone-field.open').removeClass('open') 1331 .find('.phone-field__code-toggle').attr('aria-expanded', 'false'); 1332 }, 1333 updateHiddenValue: function ($field) { 1334 const dialCode = $field.find('.phone-field__dial-code').text().trim(); 1335 const digits = $field.find('.phone-field__input').val().replace(/\D/g, ''); 1336 1337 $field.find('.phone-field__hidden-input').val(dialCode + digits).trigger('change'); 1338 }, 1339 selectOption: function ($field, $option) { 1340 $field.find('.phone-field__option').removeClass('active'); 1341 $option.addClass('active'); 1342 1343 $field.find('.phone-field__flag').html($option.find('.phone-field__option-flag').html()); 1344 $field.find('.phone-field__dial-code').text($option.data('dial-code')); 1345 1346 PhoneCodeSelector.updateHiddenValue($field); 1347 }, 1348 init: function () { 1349 $(document).on('click', '.phone-field__code-toggle', function (e) { 1350 e.stopPropagation(); 1351 const $toggle = $(this); 1352 const $field = $toggle.closest('.phone-field'); 1353 const isOpen = $field.hasClass('open'); 1354 1355 PhoneCodeSelector.closeAll(); 1356 1357 if (!isOpen) { 1358 $field.addClass('open'); 1359 $toggle.attr('aria-expanded', 'true'); 1360 $field.find('.phone-field__dropdown .search-input').val('').trigger('input').focus(); 1361 } 1362 }); 1363 1364 $(document).on('input', '.phone-field__dropdown .search-input', function () { 1365 const searchTerm = $(this).val().toLowerCase(); 1366 const $dropdown = $(this).closest('.phone-field__dropdown'); 1367 1368 $dropdown.find('.phone-field__option').each(function () { 1369 const $option = $(this); 1370 const haystack = ($option.find('.phone-field__option-name').text() + ' ' + $option.find('.phone-field__option-code').text()).toLowerCase(); 1371 1372 $option.toggle(haystack.includes(searchTerm)); 1373 }); 1374 }); 1375 1376 $(document).on('click', '.phone-field__option', function () { 1377 const $option = $(this); 1378 const $field = $option.closest('.phone-field'); 1379 1380 PhoneCodeSelector.selectOption($field, $option); 1381 PhoneCodeSelector.closeAll(); 1382 $field.find('.phone-field__input').trigger('focus'); 1383 }); 1384 1385 // Keep the phone country code in sync with the nationality selector 1386 $(document).on('change', '#selected-country', function () { 1387 const code = $(this).val(); 1388 const $field = $(this).closest('form').find('.phone-field'); 1389 1390 if (!code || !$field.length) { 1391 return; 1392 } 1393 1394 const $option = $field.find(`.phone-field__option[data-code="${code}"]`); 1395 1396 if ($option.length) { 1397 PhoneCodeSelector.selectOption($field, $option); 1398 } 1399 }); 1400 1401 $(document).on('input', '.phone-field__input', function () { 1402 PhoneCodeSelector.updateHiddenValue($(this).closest('.phone-field')); 1403 }); 1404 1405 $(document).on('keydown', '.phone-field__dropdown .search-input', function (e) { 1406 if (e.key === 'Enter') { 1407 e.preventDefault(); 1408 1409 const $visible = $(this).closest('.phone-field__dropdown').find('.phone-field__option:visible'); 1410 1411 if ($visible.length === 1) { 1412 $visible.click(); 1413 } 1414 } 1415 }); 1416 1417 $(document).on('click', function (e) { 1418 if (!$(e.target).closest('.phone-field').length) { 1419 PhoneCodeSelector.closeAll(); 1420 } 1421 }); 1422 1423 $(document).on('keydown', function (e) { 1424 if (e.key === 'Escape') { 1425 PhoneCodeSelector.closeAll(); 1426 } 1427 }); 1428 1429 $('.phone-field').each(function () { 1430 PhoneCodeSelector.updateHiddenValue($(this)); 1431 }); 1432 } 1433}; 1434 1435$(document).ready(function () { 1436 Search.init(); 1437 PassengerSelector.init(); 1438 BudgetRangeSlider.init(); 1439 RoomTypeSelector.init();
1440 ReadMore.init(); 1441 SuggestionField.init(); 1442 LanguageTooltip.init(); 1443 PhoneCodeSelector.init(); 1444 // initScrollArrows(); 1445});
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.