PageSourceSearch

https://en.tours.japantravel.com/js/portal.js?1789621940

js japantravel.com collected 2026-09-24 17:19:23 UTC 55,588 bytes, 1,445 lines download raw bytes

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.