PageSourceSearch

https://www.cornishstays.co.uk/wp-content/plugins/cornish-stays-su…/js/availability-search.js?ver=1.9.0

js cornishstays.co.uk collected 2026-10-04 08:16:52 UTC 91,304 bytes, 3,265 lines download raw bytes

1/**
2 * Cornish Stays Availability Search
3 *
4 * Results mode:
5 * - all active mapped properties show on initial load
6 * - filters update results via AJAX
7 * - valid dates trigger cached availability/pricing
8 * - map stays hidden until Show Map is clicked
9 * - map markers update with AJAX results
10 * - card hover focuses the matching map marker when map is open
11 *
12 * Home mode:
13 * - keeps Search Properties button
14 * - submits selections to /properties/
15 */
16const CSAvailabilitySuperControlCart = {
17    ready: false,
18    pending: null,
19    pendingTimer: null
20};
21
22
23window.addEventListener(
24    "message",
25    function (event) {
26
27        let data =
28            event
29                ? event.data
30                : null;
31
32        if (typeof data === "string") {
33            try {
34                data = JSON.parse(data);
35            } catch (error) {
36                return;
37            }
38        }
39
40        if (
41            !data
42            ||
43            typeof data !== "object"
44            ||
45            !data.action
46        ) {
47            return;
48        }
49
50        if (data.action === "pickerLoaded") {
51
52            CSAvailabilitySuperControlCart.ready =
53                true;
54
55            if (
56                CSAvailabilitySuperControlCart.pending
57            ) {
58
59                const pending =
60                    CSAvailabilitySuperControlCart.pending;
61
62                CSAvailabilitySuperControlCart.pending =
63                    null;
64
65                clearTimeout(
66                    CSAvailabilitySuperControlCart.pendingTimer
67                );
68
69                window.postMessage(
70                    pending.details,
71                    "*"
72                );
73
74                restoreBookingButton(
75                    pending.button
76                );
77            }
78        }
79
80        if (data.action === "closePopup") {
81
82            document
83                .querySelectorAll(
84                    ".cs-availability-card__book.is-booking"
85                )
86                .forEach(
87                    function (button) {
88                        restoreBookingButton(
89                            button
90                        );
91                    }
92                );
93        }
94    }
95);
96
97
98function restoreBookingButton(button) {
99
100    if (!button) {
101        return;
102    }
103
104    button.disabled = false;
105    button.classList.remove(
106        "is-booking"
107    );
108
109    button.textContent =
110        button.getAttribute(
111            "data-default-label"
112        )
113        ||
114        "Book Now";
115}
116
117
118function initialiseCSAvailabilitySearch() {
119
120    document
121        .querySelectorAll(".cs-availability-search")
122        .forEach(function (root) {
123
124            if (root.dataset.csAvailabilityInitialised === "true") {
125                return;
126            }
127
128            root.dataset.csAvailabilityInitialised = "true";
129            initialiseSearch(root);
130        });
131
132
133    function initialiseSearch(root) {
134
135        const mode =
136            root.getAttribute("data-mode")
137            ||
138            "results";
139
140        const state = {
141            debounceTimer: null,
142            abortController: null,
143            map: null,
144            markerLayer: null,
145            markersByPostId: new Map(),
146            markerData: readInitialMarkerData(root),
147            mapInitialised: false
148        };
149
150        root._csAvailabilityState = state;
151
152        if (mode === "homepage-results") {
153            bindBookingCart(root, null);
154            bindViewPropertyButtons(root);
155            bindPremiumHomepageResultActions(root);
156            bindAvailabilitySorting(root);
157            bindMapToggle(root, state);
158            bindCardInteractions(root, state);
159            bindPremiumHomepageResultsReset(root, state);
160            initPropertyExcerptCrop(root);
161            return;
162        }
163
164        const form =
165            root.querySelector(
166                ".cs-availability-search__form"
167            );
168
169        if (!form) {
170            return;
171        }
172
173        if (mode === "homepage") {
174
175            [
176                root.closest(".et_pb_module"),
177                root.closest(".et_pb_column"),
178                root.closest(".et_pb_row"),
179                root.closest(".et_pb_section")
180            ].forEach(function (host) {
181                if (host) {
182                    host.classList.add("cs-availability-layer-host");
183                }
184            });
185
186            initialisePremiumHomepageControls(
187                root,
188                form
189            );
190
191            bindPremiumHomepageSearch(
192                root,
193                form
194            );
195
196            return;
197        }
198
199        initialiseFormControls(
200            root,
201            form
202        );
203
204        if (mode !== "results") {
205            return;
206        }
207
208        bindBookingCart(
209            root,
210            form
211        );
212
213        bindViewPropertyButtons(root);
214
215        bindLiveFiltering(
216            root,
217            form,
218            state
219        );
220
221        bindMapToggle(
222            root,
223            state
224        );
225
226        bindCardInteractions(
227            root,
228            state
229        );
230
231        initPropertyExcerptCrop(root);
232    }
233
234
235    /* =====================================================
236       Form controls
237       ===================================================== */
238
239    function initialiseFormControls(root, form) {
240
241        const area =
242            form.querySelector(
243                'select[name="cs_area"]'
244            );
245
246        const town =
247            form.querySelector(
248                'select[name="cs_town"]'
249            );
250
251        const arrival =
252            form.querySelector(
253                'input[name="cs_arrival"]'
254            );
255
256        const departure =
257            form.querySelector(
258                'input[name="cs_departure"]'
259            );
260
261        const filterToggle =
262            form.querySelector(
263                ".cs-availability-search__filter-toggle"
264            );
265
266        const filters =
267            form.querySelector(
268                ".cs-availability-search__filters"
269            );
270
271        const clearButton =
272            form.querySelector(
273                ".cs-availability-search__clear"
274            );
275
276
277        function getConfig() {
278
279            return (
280                window.CSAvailabilitySearch
281                &&
282                CSAvailabilitySearch.locations
283            )
284                ? CSAvailabilitySearch.locations
285                : null;
286        }
287
288
289        function rebuildTowns(
290            keepCurrent
291        ) {
292
293            if (!area || !town) {
294                return;
295            }
296
297            const config =
298                getConfig();
299
300            if (
301                !config
302                ||
303                !config.towns
304            ) {
305                return;
306            }
307
308            const current =
309                keepCurrent
310                    ? town.value
311                    : "";
312
313            const key =
314                area.value === "north"
315                ||
316                area.value === "south"
317                    ? area.value
318                    : "all";
319
320            const towns =
321                config.towns[key]
322                ||
323                [];
324
325            town.innerHTML = "";
326
327            town.add(
328                new Option(
329                    "All Towns",
330                    ""
331                )
332            );
333
334            towns.forEach(
335                function (item) {
336                    town.add(
337                        new Option(
338                            String(
339                                item.label
340                            ),
341                            String(
342                                item.value
343                            )
344                        )
345                    );
346                }
347            );
348
349            if (
350                current
351                &&
352                Array.from(
353                    town.options
354                ).some(
355                    function (option) {
356                        return option.value === current;
357                    }
358                )
359            ) {
360                town.value =
361                    current;
362            }
363        }
364
365
366        if (area && town) {
367
368            area.addEventListener(
369                "change",
370                function () {
371                    rebuildTowns(false);
372                }
373            );
374
375            rebuildTowns(true);
376        }
377
378
379        function localDateString(date) {
380
381            return [
382                date.getFullYear(),
383                String(
384                    date.getMonth() + 1
385                ).padStart(2, "0"),
386                String(
387                    date.getDate()
388                ).padStart(2, "0")
389            ].join("-");
390        }
391
392
393        function updateDateLimits() {
394
395            if (!arrival || !departure) {
396                return;
397            }
398
399            const today =
400                new Date();
401
402            arrival.min =
403                localDateString(today);
404
405            if (!arrival.value) {
406                departure.removeAttribute(
407                    "min"
408                );
409                return;
410            }
411
412            const next =
413                new Date(
414                    arrival.value
415                    +
416                    "T00:00:00"
417                );
418
419            next.setDate(
420                next.getDate() + 1
421            );
422
423            const minimum =
424                localDateString(next);
425
426            departure.min =
427                minimum;
428
429            if (
430                departure.value
431                &&
432                departure.value < minimum
433            ) {
434                departure.value = "";
435            }
436        }
437
438
439        initialiseDateDisplay(arrival);
440        initialiseDateDisplay(departure);
441
442        if (arrival && departure) {
443
444            arrival.addEventListener(
445                "change",
446                updateDateLimits
447            );
448
449            departure.addEventListener(
450                "change",
451                updateDateLimits
452            );
453
454            updateDateLimits();
455        }
456
457
458        if (filterToggle && filters) {
459
460            filterToggle.addEventListener(
461                "click",
462                function () {
463
464                    const open =
465                        filters.classList.toggle(
466                            "is-open"
467                        );
468
469                    filterToggle.setAttribute(
470                        "aria-expanded",
471                        open
472                            ? "true"
473                            : "false"
474                    );
475                }
476            );
477        }
478
479
480        form.addEventListener(
481            "change",
482            function (event) {
483
484                const input =
485                    event.target;
486
487                if (
488                    input
489                    &&
490                    input.matches(
491                        'input[type="checkbox"]'
492                    )
493                ) {
494
495                    const tile =
496                        input.closest(
497                            ".ps-overlay-variable-item"
498                        );
499
500                    if (tile) {
501                        tile.classList.toggle(
502                            "checked",
503                            input.checked
504                        );
505                    }
506
507                    updateFilterCount(
508                        form
509                    );
510                }
511            }
512        );
513
514
515        if (clearButton) {
516
517            clearButton.addEventListener(
518                "click",
519                function () {
520
521                    if (area) {
522                        area.value = "all";
523                    }
524
525                    if (town) {
526                        rebuildTowns(false);
527                    }
528
529                    form
530                        .querySelectorAll(
531                            'input[type="date"]'
532                        )
533                        .forEach(
534                            function (input) {
535                                input.value = "";
536                                input.dispatchEvent(
537                                    new Event(
538                                        "change",
539                                        {
540                                            bubbles: false
541                                        }
542                                    )
543                                );
544                            }
545                        );
546
547                    const flexibility =
548                        form.querySelector(
549                            'select[name="cs_date_flexibility"]'
550                        );
551
552                    if (flexibility) {
553                        flexibility.value = "0";
554                    }
555
556                    const guests =
557                        form.querySelector(
558                            'select[name="cs_guests"]'
559                        );
560
561                    if (guests) {
562                        guests.value = "0";
563                    }
564
565                    form
566                        .querySelectorAll(
567                            'input[type="checkbox"]'
568                        )
569                        .forEach(
570                            function (input) {
571                                input.checked = false;
572
573                                const tile =
574                                    input.closest(
575                                        ".ps-overlay-variable-item"
576                                    );
577
578                                if (tile) {
579                                    tile.classList.remove(
580                                        "checked"
581                                    );
582                                }
583                            }
584                        );
585
586                    if (filters) {
587                        filters.classList.remove(
588                            "is-open"
589                        );
590                    }
591
592                    if (filterToggle) {
593                        filterToggle.setAttribute(
594                            "aria-expanded",
595                            "false"
596                        );
597                    }
598
599                    updateFilterCount(
600                        form
601                    );
602
603                    updateDateLimits();
604
605                    form.dispatchEvent(
606                        new CustomEvent(
607                            "csAvailabilityClear"
608                        )
609                    );
610                }
611            );
612        }
613    }
614
615
616    function initialiseDateDisplay(input) {
617
618        if (!input) {
619            return;
620        }
621
622        const field =
623            input.closest(
624                ".cs-availability-search__date-field"
625            );
626
627        const display =
628            field
629                ? field.querySelector(
630                    ".cs-availability-search__date-display"
631                )
632                : null;
633
634        if (!display) {
635            return;
636        }
637
638        function formatValue(value) {
639
640            if (!value) {
641                return "";
642            }
643
644            const parts =
645                value.split("-");
646
647            if (parts.length !== 3) {
648                return value;
649            }
650
651            const months = [
652                "Jan", "Feb", "Mar", "Apr",
653                "May", "Jun", "Jul", "Aug",
654                "Sep", "Oct", "Nov", "Dec"
655            ];
656
657            const year =
658                Number(parts[0]);
659
660            const month =
661                Number(parts[1]);
662
663            const day =
664                Number(parts[2]);
665
666            if (
667                !year
668                ||
669                month < 1
670                ||
671                month > 12
672                ||
673                day < 1
674                ||
675                day > 31
676            ) {
677                return value;
678            }
679
680            return (
681                day
682                +
683                " "
684                +
685                months[month - 1]
686                +
687                " "
688                +
689                year
690            );
691        }
692
693        function syncDisplay() {
694            display.value =
695                formatValue(
696                    input.value
697                );
698        }
699
700        function openPicker() {
701
702            /*
703             * showPicker() works when called directly from a genuine
704             * user click/keypress. If a browser does not support it,
705             * fall back to focusing/clicking the native date input.
706             */
707            if (
708                typeof input.showPicker
709                ===
710                "function"
711            ) {
712                try {
713                    input.showPicker();
714                    return;
715                } catch (error) {
716                    // Fall through to the focus/click fallback.
717                }
718            }
719
720            try {
721                input.focus();
722                input.click();
723            } catch (error) {
724                input.focus();
725            }
726        }
727
728        display.addEventListener(
729            "click",
730            openPicker
731        );
732
733        display.addEventListener(
734            "keydown",
735            function (event) {
736
737                if (
738                    event.key === "Enter"
739                    ||
740                    event.key === " "
741                ) {
742                    event.preventDefault();
743                    openPicker();
744                }
745            }
746        );
747
748        input.addEventListener(
749            "change",
750            syncDisplay
751        );
752
753        input.addEventListener(
754            "input",
755            syncDisplay
756        );
757
758        syncDisplay();
759    }
760
761
762    function updateFilterCount(form) {
763
764        const count =
765            form.querySelectorAll(
766                '.cs-availability-search__filters input[type="checkbox"]:checked'
767            ).length;
768
769        const badge =
770            form.querySelector(
771                ".cs-availability-search__filter-count"
772            );
773
774        if (!badge) {
775            return;
776        }
777
778        badge.textContent =
779            String(count);
780
781        badge.hidden =
782            count === 0;
783    }
784
785
786    /* =====================================================
787       Premium homepage state / sharing helpers
788       ===================================================== */
789
790    const premiumStateKey = 'cornishStaysAvailabilitySearch';
791
792    function serialisePremiumSearch(form) {
793        const params = new URLSearchParams();
794
795        new FormData(form).forEach(function (value, key) {
796            const stringValue = String(value);
797            const isDefault =
798                (key === 'cs_area' && stringValue === 'all')
799                || (key === 'cs_town' && stringValue === '')
800                || (key === 'cs_arrival' && stringValue === '')
801                || (key === 'cs_departure' && stringValue === '')
802                || (key === 'cs_date_flexibility' && stringValue === '0')
803                || (key === 'cs_guests' && stringValue === '0');
804
805            if (!isDefault) {
806                params.append(key, stringValue);
807            }
808        });
809
810        return params;
811    }
812
813    function savePremiumSearchState(form) {
814        try {
815            window.sessionStorage.setItem(
816                premiumStateKey,
817                serialisePremiumSearch(form).toString()
818            );
819        } catch (error) {
820            /* Storage is an enhancement only. */
821        }
822    }
823
824    function applyPremiumSearchState(form, params) {
825        if (!params) {
826            return;
827        }
828
829        ['cs_area', 'cs_town', 'cs_arrival', 'cs_departure', 'cs_guests'].forEach(function (name) {
830            const input = form.querySelector('[name="' + name + '"]');
831            if (input && params.has(name)) {
832                input.value = params.get(name) || '';
833            }
834        });
835
836        const flex = params.get('cs_date_flexibility');
837        if (flex !== null) {
838            const radio = form.querySelector('[name="cs_date_flexibility"][value="' + flex + '"]');
839            if (radio) {
840                radio.checked = true;
841            }
842        }
843
844        ['cs_pets', 'cs_hot_tub', 'cs_sauna', 'cs_ev_charger', 'cs_accessible'].forEach(function (name) {
845            const input = form.querySelector('[name="' + name + '"]');
846            if (input) {
847                input.checked = params.get(name) === '1';
848            }
849        });
850    }
851
852    function restorePremiumSearchState(form) {
853        const urlParams = new URLSearchParams(window.location.search);
854        const hasSharedState = Array.from(urlParams.keys()).some(function (key) {
855            return key.indexOf('cs_') === 0;
856        });
857
858        if (hasSharedState) {
859            applyPremiumSearchState(form, urlParams);
860            return true;
861        }
862
863        try {
864            const stored = window.sessionStorage.getItem(premiumStateKey);
865            if (stored) {
866                applyPremiumSearchState(form, new URLSearchParams(stored));
867            }
868        } catch (error) {
869            /* Storage is an enhancement only. */
870        }
871
872        return false;
873    }
874
875    /* =====================================================
876       Premium homepage controls
877       ===================================================== */
878
879    function initialisePremiumHomepageControls(root, form) {
880
881        const areaInput =
882            form.querySelector('[name="cs_area"]');
883
884        const townInput =
885            form.querySelector('[name="cs_town"]');
886
887        const arrivalInput =
888            form.querySelector('[name="cs_arrival"]');
889
890        const departureInput =
891            form.querySelector('[name="cs_departure"]');
892
893        const guestInput =
894            form.querySelector('[name="cs_guests"]');
895
896        const openedFromSharedUrl = restorePremiumSearchState(form);
897        form.dataset.csOpenedFromSharedUrl = openedFromSharedUrl ? '1' : '0';
898
899        const destinationSummary =
900            form.querySelector('[data-destination-summary]');
901
902        const dateSummary =
903            form.querySelector('[data-date-summary]');
904
905        const guestSummary =
906            form.querySelector('[data-guest-summary]');
907
908        const arrivalLabel =
909            form.querySelector('[data-arrival-label]');
910
911        const departureLabel =
912            form.querySelector('[data-departure-label]');
913
914        const nightsLabel =
915            form.querySelector('[data-nights-label]');
916
917        const guestCount =
918            form.querySelector('[data-guest-count]');
919
920        const triggers =
921            Array.from(
922                form.querySelectorAll('[data-premium-panel]')
923            );
924
925        const panels =
926            Array.from(
927                form.querySelectorAll('[data-panel-name]')
928            );
929
930
931        function closePanels() {
932
933            panels.forEach(function (panel) {
934                panel.hidden = true;
935            });
936
937            triggers.forEach(function (trigger) {
938                trigger.setAttribute(
939                    'aria-expanded',
940                    'false'
941                );
942            });
943
944            form.classList.remove('is-panel-open');
945        }
946
947
948        function openPanel(name) {
949
950            const panel =
951                form.querySelector(
952                    '[data-panel-name="' + name + '"]'
953                );
954
955            if (!panel) {
956                return;
957            }
958
959            const isAlreadyOpen = !panel.hidden;
960
961            closePanels();
962
963            if (isAlreadyOpen) {
964                return;
965            }
966
967            panel.hidden = false;
968            form.classList.add('is-panel-open');
969
970            const trigger =
971                form.querySelector(
972                    '[data-premium-panel="' + name + '"]'
973                );
974
975            if (trigger) {
976                trigger.setAttribute(
977                    'aria-expanded',
978                    'true'
979                );
980            }
981        }
982
983
984        triggers.forEach(function (trigger) {
985            trigger.addEventListener(
986                'click',
987                function () {
988                    openPanel(
989                        trigger.getAttribute('data-premium-panel')
990                    );
991                }
992            );
993        });
994
995        form
996            .querySelectorAll('.cs-premium-search__panel-close, .cs-premium-search__done')
997            .forEach(function (button) {
998                button.addEventListener('click', closePanels);
999            });
1000
1001        /*
1002         * Keep the panel open while the guest interacts with it. The date
1003         * calendar redraws its buttons after a selection, which removes the
1004         * original event.target before the document-level click handler runs.
1005         * composedPath() preserves the original click path and prevents that
1006         * redraw from being mistaken for an outside click.
1007         */
1008        document.addEventListener(
1009            'click',
1010            function (event) {
1011                const path =
1012                    typeof event.composedPath === 'function'
1013                        ? event.composedPath()
1014                        : [];
1015
1016                const clickedInsideForm =
1017                    path.length > 0
1018                        ? path.indexOf(form) !== -1
1019                        : form.contains(event.target);
1020
1021                if (!clickedInsideForm) {
1022                    closePanels();
1023                }
1024            }
1025        );
1026
1027        document.addEventListener(
1028            'keydown',
1029            function (event) {
1030                if (event.key === 'Escape') {
1031                    closePanels();
1032                }
1033            }
1034        );
1035
1036
1037        /* Destination hierarchy. */
1038        form
1039            .querySelectorAll('[data-area][data-town]')
1040            .forEach(function (button) {
1041                button.addEventListener(
1042                    'click',
1043                    function () {
1044
1045                        if (areaInput) {
1046                            areaInput.value =
1047                                button.getAttribute('data-area') || 'all';
1048                        }
1049
1050                        if (townInput) {
1051                            townInput.value =
1052                                button.getAttribute('data-town') || '';
1053                        }
1054
1055                        if (destinationSummary) {
1056                            destinationSummary.textContent =
1057                                button.getAttribute('data-label')
1058                                ||
1059                                'All Cornwall';
1060                        }
1061
1062                        form
1063                            .querySelectorAll('[data-area][data-town]')
1064                            .forEach(function (item) {
1065                                item.classList.toggle(
1066                                    'is-selected',
1067                                    item === button
1068                                );
1069                            });
1070
1071                        closePanels();
1072                    }
1073                );
1074            });
1075
1076
1077        const selectedDestinationButton =
1078            form.querySelector(
1079                '[data-area="' + (areaInput ? areaInput.value : 'all') + '"][data-town="' + (townInput ? townInput.value : '') + '"]'
1080            );
1081
1082        if (selectedDestinationButton) {
1083            selectedDestinationButton.classList.add('is-selected');
1084            if (destinationSummary) {
1085                destinationSummary.textContent =
1086                    selectedDestinationButton.getAttribute('data-label') || 'All Cornwall';
1087            }
1088        }
1089
1090        function parseIsoDate(value) {
1091            if (!value) {
1092                return null;
1093            }
1094
1095            const parts = value.split('-');
1096
1097            if (parts.length !== 3) {
1098                return null;
1099            }
1100
1101            const date = new Date(
1102                Number(parts[0]),
1103                Number(parts[1]) - 1,
1104                Number(parts[2])
1105            );
1106
1107            return Number.isNaN(date.getTime())
1108                ? null
1109                : date;
1110        }
1111
1112
1113        function isoDate(date) {
1114            return [
1115                date.getFullYear(),
1116                String(date.getMonth() + 1).padStart(2, '0'),
1117                String(date.getDate()).padStart(2, '0')
1118            ].join('-');
1119        }
1120
1121
1122        function shortDate(date, includeYear) {
1123
1124            if (!date) {
1125                return '';
1126            }
1127
1128            return new Intl.DateTimeFormat(
1129                'en-GB',
1130                includeYear
1131                    ? { day: 'numeric', month: 'short', year: 'numeric' }
1132                    : { day: 'numeric', month: 'short' }
1133            ).format(date);
1134        }
1135
1136
1137        function updateDateSummary() {
1138
1139            const arrival =
1140                arrivalInput
1141                    ? parseIsoDate(arrivalInput.value)
1142                    : null;
1143
1144            const departure =
1145                departureInput
1146                    ? parseIsoDate(departureInput.value)
1147                    : null;
1148
1149            if (arrivalLabel) {
1150                arrivalLabel.textContent =
1151                    arrival
1152                        ? shortDate(arrival, true)
1153                        : 'Add date';
1154            }
1155
1156            if (departureLabel) {
1157                departureLabel.textContent =
1158                    departure
1159                        ? shortDate(departure, true)
1160                        : 'Add date';
1161            }
1162
1163            if (arrival && departure) {
1164
1165                if (dateSummary) {
1166                    dateSummary.textContent =
1167                        shortDate(arrival, false)
1168                        +
1169                        ' — '
1170                        +
1171                        shortDate(departure, true);
1172                }
1173
1174                const nights =
1175                    Math.round(
1176                        (departure.getTime() - arrival.getTime())
1177                        /
1178                        86400000
1179                    );
1180
1181                if (nightsLabel) {
1182                    nightsLabel.textContent =
1183                        nights
1184                        +
1185                        (nights === 1 ? ' night' : ' nights');
1186                    nightsLabel.hidden = false;
1187                }
1188
1189            } else {
1190
1191                if (dateSummary) {
1192                    dateSummary.textContent = 'Add dates';
1193                }
1194
1195                if (nightsLabel) {
1196                    nightsLabel.hidden = true;
1197                }
1198            }
1199        }
1200
1201
1202        let premiumCalendar = null;
1203        const calendarWrap =
1204            form.querySelector('.cs-premium-search__calendar-wrap');
1205
1206        if (calendarWrap) {
1207
1208            const today = new Date();
1209            today.setHours(0, 0, 0, 0);
1210
1211            const initialArrival =
1212                arrivalInput
1213                    ? parseIsoDate(arrivalInput.value)
1214                    : null;
1215
1216            let viewDate = new Date(
1217                (initialArrival || today).getFullYear(),
1218                (initialArrival || today).getMonth(),
1219                1
1220            );
1221
1222            const monthNames = [
1223                'January', 'February', 'March', 'April', 'May', 'June',
1224                'July', 'August', 'September', 'October', 'November', 'December'
1225            ];
1226
1227            const weekdayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
1228
1229            function sameDay(a, b) {
1230                return !!a && !!b
1231                    && a.getFullYear() === b.getFullYear()
1232                    && a.getMonth() === b.getMonth()
1233                    && a.getDate() === b.getDate();
1234            }
1235
1236            function dateValue(date) {
1237                return new Date(
1238                    date.getFullYear(),
1239                    date.getMonth(),
1240                    date.getDate()
1241                ).getTime();
1242            }
1243
1244            function monthsToShow() {
1245                return window.matchMedia('(max-width: 767px)').matches ? 1 : 2;
1246            }
1247
1248            function renderMonth(year, month) {
1249
1250                const monthEl = document.createElement('div');
1251                monthEl.className = 'cs-premium-calendar__month';
1252
1253                const title = document.createElement('div');
1254                title.className = 'cs-premium-calendar__month-title';
1255                title.textContent = monthNames[month] + ' ' + year;
1256                monthEl.appendChild(title);
1257
1258                const weekdays = document.createElement('div');
1259                weekdays.className = 'cs-premium-calendar__weekdays';
1260
1261                weekdayNames.forEach(function (day) {
1262                    const span = document.createElement('span');
1263                    span.textContent = day;
1264                    weekdays.appendChild(span);
1265                });
1266
1267                monthEl.appendChild(weekdays);
1268
1269                const days = document.createElement('div');
1270                days.className = 'cs-premium-calendar__days';
1271
1272                const firstDay = new Date(year, month, 1);
1273                const mondayOffset = (firstDay.getDay() + 6) % 7;
1274                const daysInMonth = new Date(year, month + 1, 0).getDate();
1275
1276                for (let i = 0; i < mondayOffset; i += 1) {
1277                    const spacer = document.createElement('span');
1278                    spacer.className = 'cs-premium-calendar__spacer';
1279                    days.appendChild(spacer);
1280                }
1281
1282                const arrival = arrivalInput ? parseIsoDate(arrivalInput.value) : null;
1283                const departure = departureInput ? parseIsoDate(departureInput.value) : null;
1284
1285                for (let day = 1; day <= daysInMonth; day += 1) {
1286                    const date = new Date(year, month, day);
1287                    const button = document.createElement('button');
1288                    button.type = 'button';
1289                    button.className = 'cs-premium-calendar__day';
1290                    button.textContent = String(day);
1291                    button.setAttribute('aria-label', shortDate(date, true));
1292                    button.setAttribute('data-date', isoDate(date));
1293
1294                    if (dateValue(date) < dateValue(today)) {
1295                        button.disabled = true;
1296                        button.classList.add('is-disabled');
1297                    }
1298
1299                    if (sameDay(date, arrival)) {
1300                        button.classList.add('is-arrival');
1301                    }
1302
1303                    if (sameDay(date, departure)) {
1304                        button.classList.add('is-departure');
1305                    }
1306
1307                    if (
1308                        arrival
1309                        && departure
1310                        && dateValue(date) > dateValue(arrival)
1311                        && dateValue(date) < dateValue(departure)
1312                    ) {
1313                        button.classList.add('is-in-range');
1314                    }
1315
1316                    button.addEventListener('mouseenter', function () {
1317                        const currentArrival = arrivalInput ? parseIsoDate(arrivalInput.value) : null;
1318                        const currentDeparture = departureInput ? parseIsoDate(departureInput.value) : null;
1319
1320                        if (currentArrival && !currentDeparture && dateValue(date) > dateValue(currentArrival)) {
1321                            updateCalendarPreview(date);
1322                        }
1323                    });
1324
1325                    button.addEventListener('focus', function () {
1326                        const currentArrival = arrivalInput ? parseIsoDate(arrivalInput.value) : null;
1327                        const currentDeparture = departureInput ? parseIsoDate(departureInput.value) : null;
1328
1329                        if (currentArrival && !currentDeparture && dateValue(date) > dateValue(currentArrival)) {
1330                            updateCalendarPreview(date);
1331                        }
1332                    });
1333
1334                    button.addEventListener('click', function () {
1335                        const currentArrival = arrivalInput ? parseIsoDate(arrivalInput.value) : null;
1336                        const currentDeparture = departureInput ? parseIsoDate(departureInput.value) : null;
1337
1338                        if (
1339                            !currentArrival
1340                            || currentDeparture
1341                            || dateValue(date) <= dateValue(currentArrival)
1342                        ) {
1343                            if (arrivalInput) {
1344                                arrivalInput.value = isoDate(date);
1345                            }
1346                            if (departureInput) {
1347                                departureInput.value = '';
1348                            }
1349                        } else {
1350                            if (departureInput) {
1351                                departureInput.value = isoDate(date);
1352                            }
1353                        }
1354
1355                        updateDateSummary();
1356                        renderCalendar();
1357                    });
1358
1359                    days.appendChild(button);
1360                }
1361
1362                monthEl.appendChild(days);
1363                return monthEl;
1364            }
1365
1366            function updateCalendarPreview(hoverDate) {
1367                const arrival = arrivalInput ? parseIsoDate(arrivalInput.value) : null;
1368
1369                calendarWrap
1370                    .querySelectorAll('.cs-premium-calendar__day')
1371                    .forEach(function (dayButton) {
1372                        dayButton.classList.remove('is-preview-range', 'is-preview-departure');
1373
1374                        if (!arrival || !hoverDate) {
1375                            return;
1376                        }
1377
1378                        const dayDate = parseIsoDate(dayButton.getAttribute('data-date'));
1379                        if (!dayDate) {
1380                            return;
1381                        }
1382
1383                        if (dateValue(dayDate) > dateValue(arrival) && dateValue(dayDate) < dateValue(hoverDate)) {
1384                            dayButton.classList.add('is-preview-range');
1385                        }
1386
1387                        if (sameDay(dayDate, hoverDate)) {
1388                            dayButton.classList.add('is-preview-departure');
1389                        }
1390                    });
1391            }
1392
1393            function renderCalendar() {
1394                calendarWrap.innerHTML = '';
1395
1396                const toolbar = document.createElement('div');
1397                toolbar.className = 'cs-premium-calendar__toolbar';
1398
1399                const previous = document.createElement('button');
1400                previous.type = 'button';
1401                previous.className = 'cs-premium-calendar__nav cs-premium-calendar__nav--prev';
1402                previous.setAttribute('aria-label', 'Previous month');
1403                previous.innerHTML = '&#8249;';
1404
1405                const next = document.createElement('button');
1406                next.type = 'button';
1407                next.className = 'cs-premium-calendar__nav cs-premium-calendar__nav--next';
1408                next.setAttribute('aria-label', 'Next month');
1409                next.innerHTML = '&#8250;';
1410
1411                const earliestMonth = new Date(today.getFullYear(), today.getMonth(), 1);
1412                previous.disabled = viewDate.getTime() <= earliestMonth.getTime();
1413
1414                previous.addEventListener('click', function () {
1415                    viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1);
1416                    renderCalendar();
1417                });
1418
1419                next.addEventListener('click', function () {
1420                    viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1);
1421                    renderCalendar();
1422                });
1423
1424                toolbar.appendChild(previous);
1425                toolbar.appendChild(next);
1426                calendarWrap.appendChild(toolbar);
1427
1428                const months = document.createElement('div');
1429                months.className = 'cs-premium-calendar__months';
1430
1431                for (let i = 0; i < monthsToShow(); i += 1) {
1432                    const date = new Date(viewDate.getFullYear(), viewDate.getMonth() + i, 1);
1433                    months.appendChild(renderMonth(date.getFullYear(), date.getMonth()));
1434                }
1435
1436                calendarWrap.appendChild(months);
1437
1438                months.addEventListener('mouseleave', function () {
1439                    updateCalendarPreview(null);
1440                });
1441            }
1442
1443            let resizeTimer = null;
1444            window.addEventListener('resize', function () {
1445                clearTimeout(resizeTimer);
1446                resizeTimer = setTimeout(renderCalendar, 120);
1447            });
1448
1449            premiumCalendar = {
1450                clear: function () {
1451                    if (arrivalInput) {
1452                        arrivalInput.value = '';
1453                    }
1454                    if (departureInput) {
1455                        departureInput.value = '';
1456                    }
1457                    viewDate = new Date(today.getFullYear(), today.getMonth(), 1);
1458                    renderCalendar();
1459                },
1460                render: renderCalendar
1461            };
1462
1463            renderCalendar();
1464        }
1465
1466        const clearDates =
1467            form.querySelector('.cs-premium-search__clear-dates');
1468
1469        if (clearDates) {
1470            clearDates.addEventListener(
1471                'click',
1472                function () {
1473                    if (premiumCalendar) {
1474                        premiumCalendar.clear();
1475                    }
1476                    if (arrivalInput) {
1477                        arrivalInput.value = '';
1478                    }
1479                    if (departureInput) {
1480                        departureInput.value = '';
1481                    }
1482                    updateDateSummary();
1483                }
1484            );
1485        }
1486
1487        updateDateSummary();
1488
1489
1490        /* Guests and features. */
1491        function featureCount() {
1492            return form.querySelectorAll(
1493                '.cs-premium-search__feature-tile input[type="checkbox"]:checked'
1494            ).length;
1495        }
1496
1497
1498        function updateGuestSummary() {
1499
1500            let guests =
1501                guestInput
1502                    ? Number(guestInput.value) || 0
1503                    : 0;
1504
1505            guests = Math.max(0, guests);
1506
1507            if (guestCount) {
1508                guestCount.textContent = String(guests);
1509            }
1510
1511            const features = featureCount();
1512            let label =
1513                guests > 0
1514                    ? guests + (guests === 1 ? ' guest' : ' guests')
1515                    : 'Add guests & features';
1516
1517            if (features > 0) {
1518                label +=
1519                    ' · '
1520                    +
1521                    features
1522                    +
1523                    (features === 1 ? ' feature' : ' features');
1524            }
1525
1526            if (guestSummary) {
1527                guestSummary.textContent = label;
1528            }
1529        }
1530
1531
1532        form
1533            .querySelectorAll('[data-guest-step]')
1534            .forEach(function (button) {
1535                button.addEventListener(
1536                    'click',
1537                    function () {
1538
1539                        const step =
1540                            Number(
1541                                button.getAttribute('data-guest-step')
1542                            ) || 0;
1543
1544                        let value =
1545                            guestInput
1546                                ? Number(guestInput.value) || 0
1547                                : 0;
1548
1549                        value = Math.max(
1550                            0,
1551                            Math.min(30, value + step)
1552                        );
1553
1554                        if (guestInput) {
1555                            guestInput.value = String(value);
1556                        }
1557
1558                        updateGuestSummary();
1559                    }
1560                );
1561            });
1562
1563        form
1564            .querySelectorAll('.cs-premium-search__feature-tile input[type="checkbox"]')
1565            .forEach(function (input) {
1566
1567                function syncTile() {
1568                    const tile =
1569                        input.closest('.cs-premium-search__feature-tile');
1570
1571                    if (tile) {
1572                        tile.classList.toggle(
1573                            'is-selected',
1574                            input.checked
1575                        );
1576                    }
1577
1578                    updateGuestSummary();
1579                }
1580
1581                input.addEventListener('change', syncTile);
1582                syncTile();
1583            });
1584
1585        const clearFeatures =
1586            form.querySelector('.cs-premium-search__clear-features');
1587
1588        if (clearFeatures) {
1589            clearFeatures.addEventListener(
1590                'click',
1591                function () {
1592                    if (guestInput) {
1593                        guestInput.value = '0';
1594                    }
1595
1596                    form
1597                        .querySelectorAll('.cs-premium-search__feature-tile input[type="checkbox"]')
1598                        .forEach(function (input) {
1599                            input.checked = false;
1600                            input.dispatchEvent(
1601                                new Event('change', { bubbles: false })
1602                            );
1603                        });
1604
1605                    updateGuestSummary();
1606                }
1607            );
1608        }
1609
1610        updateGuestSummary();
1611
1612
1613        form.csPremiumReset = function () {
1614
1615            if (areaInput) {
1616                areaInput.value = 'all';
1617            }
1618            if (townInput) {
1619                townInput.value = '';
1620            }
1621            if (destinationSummary) {
1622                destinationSummary.textContent =
1623                    'Where would you like to stay?';
1624            }
1625
1626            if (premiumCalendar) {
1627                premiumCalendar.clear();
1628            }
1629            if (arrivalInput) {
1630                arrivalInput.value = '';
1631            }
1632            if (departureInput) {
1633                departureInput.value = '';
1634            }
1635
1636            const exact =
1637                form.querySelector(
1638                    'input[name="cs_date_flexibility"][value="0"]'
1639                );
1640            if (exact) {
1641                exact.checked = true;
1642            }
1643
1644            if (guestInput) {
1645                guestInput.value = '0';
1646            }
1647
1648            form
1649                .querySelectorAll('.cs-premium-search__feature-tile input[type="checkbox"]')
1650                .forEach(function (input) {
1651                    input.checked = false;
1652                    input.dispatchEvent(
1653                        new Event('change', { bubbles: false })
1654                    );
1655                });
1656
1657            form
1658                .querySelectorAll('[data-area][data-town]')
1659                .forEach(function (item) {
1660                    item.classList.remove('is-selected');
1661                });
1662
1663            updateDateSummary();
1664            updateGuestSummary();
1665            closePanels();
1666        };
1667    }
1668
1669
1670    function bindPremiumHomepageSearch(root, form) {
1671
1672        let searchInProgress = false;
1673
1674        function runHomepageSearch(event) {
1675
1676            if (event) {
1677                event.preventDefault();
1678                event.stopPropagation();
1679            }
1680
1681            const target =
1682                document.getElementById('cs-availability-home-results')
1683                ||
1684                document.querySelector('.cs-availability-results-target');
1685
1686            if (!target) {
1687                const existing = root.querySelector('.cs-premium-search__target-message');
1688
1689                if (!existing) {
1690                    const message = document.createElement('p');
1691                    message.className = 'cs-premium-search__target-message';
1692                    message.textContent = 'The results section is not on this page. Add [availability_results] below the hero.';
1693                    message.setAttribute('role', 'status');
1694                    form.insertAdjacentElement('afterend', message);
1695                }
1696
1697                return;
1698            }
1699
1700            if (searchInProgress) {
1701                return;
1702            }
1703
1704            searchInProgress = true;
1705            savePremiumSearchState(form);
1706
1707            const searchButton = form.querySelector('.cs-premium-search__submit');
1708            const searchButtonLabel = searchButton ? searchButton.querySelector('span:first-child') : null;
1709
1710            if (searchButton) {
1711                searchButton.disabled = true;
1712                searchButton.classList.add('is-searching');
1713            }
1714
1715            if (searchButtonLabel) {
1716                searchButtonLabel.textContent = 'Finding your stay…';
1717            }
1718
1719            const state =
1720                target._csAvailabilityState
1721                ||
1722                {
1723                    debounceTimer: null,
1724                    abortController: null,
1725                    map: null,
1726                    markerLayer: null,
1727                    markersByPostId: new Map(),
1728                    markerData: readInitialMarkerData(target),
1729                    mapInitialised: false
1730                };
1731
1732            target._csAvailabilityState = state;
1733            target.hidden = false;
1734            target.classList.add('has-results');
1735
1736            /* Reveal and move to the results immediately so the button always
1737             * gives the guest visible feedback, even while AJAX is loading. */
1738            target.scrollIntoView({
1739                behavior: 'smooth',
1740                block: 'start'
1741            });
1742
1743            Promise.resolve(
1744                fetchResults(
1745                    target,
1746                    form,
1747                    state
1748                )
1749            ).finally(function () {
1750                searchInProgress = false;
1751
1752                if (searchButton) {
1753                    searchButton.disabled = false;
1754                    searchButton.classList.remove('is-searching');
1755                }
1756
1757                if (searchButtonLabel) {
1758                    searchButtonLabel.textContent = 'Search Properties';
1759                }
1760            });
1761        }
1762
1763        form.addEventListener('submit', runHomepageSearch);
1764
1765        /* Divi/optimisation layers can occasionally interfere with native form
1766         * submission. Binding the actual search button as well gives us a
1767         * reliable explicit path without changing keyboard Enter behaviour. */
1768        const searchButton =
1769            form.querySelector('.cs-premium-search__submit');
1770
1771        if (searchButton) {
1772            searchButton.addEventListener(
1773                'click',
1774                function (event) {
1775                    runHomepageSearch(event);
1776                }
1777            );
1778        }
1779
1780        if (form.dataset.csOpenedFromSharedUrl === '1') {
1781            window.setTimeout(function () {
1782                runHomepageSearch();
1783
1784                if (window.history && window.history.replaceState) {
1785                    window.history.replaceState({}, '', window.location.pathname + window.location.hash);
1786                }
1787            }, 220);
1788        }
1789    }
1790
1791
1792    function bindPremiumHomepageResultActions(root) {
1793        root.addEventListener('click', function (event) {
1794            const propertyLink = event.target.closest('.ps-card a[href]');
1795            if (!propertyLink) {
1796                return;
1797            }
1798
1799            const searchRoot = document.querySelector('.cs-availability-search--homepage[data-mode="homepage"]');
1800            const form = searchRoot ? searchRoot.querySelector('.cs-availability-search__form') : null;
1801            if (form) {
1802                savePremiumSearchState(form);
1803            }
1804        });
1805
1806        const changeButton = root.querySelector('.cs-premium-search__change-search');
1807        const shareButton = root.querySelector('[data-share-search]');
1808
1809        if (changeButton) {
1810            changeButton.addEventListener('click', function () {
1811                const searchRoot = document.querySelector('.cs-availability-search--homepage[data-mode="homepage"]');
1812                if (searchRoot) {
1813                    searchRoot.scrollIntoView({ behavior: 'smooth', block: 'center' });
1814                }
1815            });
1816        }
1817
1818        if (shareButton) {
1819            shareButton.addEventListener('click', async function () {
1820                const searchRoot = document.querySelector('.cs-availability-search--homepage[data-mode="homepage"]');
1821                const form = searchRoot ? searchRoot.querySelector('.cs-availability-search__form') : null;
1822
1823                if (!form) {
1824                    return;
1825                }
1826
1827                const params = serialisePremiumSearch(form);
1828                const shareUrl = window.location.origin + window.location.pathname + (params.toString() ? '?' + params.toString() : '');
1829                const label = shareButton.querySelector('span');
1830                const original = label ? label.textContent : 'Share Search';
1831
1832                try {
1833                    if (navigator.share) {
1834                        await navigator.share({
1835                            title: 'Cornish Stays search',
1836                            text: 'Take a look at these Cornish Stays properties.',
1837                            url: shareUrl
1838                        });
1839                    } else if (navigator.clipboard && navigator.clipboard.writeText) {
1840                        await navigator.clipboard.writeText(shareUrl);
1841                        if (label) {
1842                            label.textContent = 'Link Copied';
1843                        }
1844                        window.setTimeout(function () {
1845                            if (label) {
1846                                label.textContent = original;
1847                            }
1848                        }, 1800);
1849                    } else {
1850                        window.prompt('Copy this search link:', shareUrl);
1851                    }
1852                } catch (error) {
1853                    if (error && error.name !== 'AbortError') {
1854                        window.prompt('Copy this search link:', shareUrl);
1855                    }
1856                }
1857            });
1858        }
1859    }
1860
1861    function bindPremiumHomepageResultsReset(root, state) {
1862
1863        const resetButton =
1864            root.querySelector('.cs-premium-search__reset-results');
1865
1866        if (!resetButton) {
1867            return;
1868        }
1869
1870        resetButton.addEventListener(
1871            'click',
1872            function () {
1873
1874                const searchRoot =
1875                    document.querySelector('.cs-availability-search--homepage[data-mode="homepage"]');
1876
1877                const form =
1878                    searchRoot
1879                        ? searchRoot.querySelector('.cs-availability-search__form')
1880                        : null;
1881
1882                if (
1883                    form
1884                    &&
1885                    typeof form.csPremiumReset === 'function'
1886                ) {
1887                    form.csPremiumReset();
1888                }
1889
1890                if (state.abortController) {
1891                    state.abortController.abort();
1892                }
1893
1894                state.markerData = [];
1895
1896                if (state.markerLayer) {
1897                    state.markerLayer.clearLayers();
1898                }
1899
1900                const mapWrap =
1901                    root.querySelector('.cs-availability-search__map-wrap');
1902
1903                const mapToggle =
1904                    root.querySelector('.cs-availability-search__map-toggle');
1905
1906                if (mapWrap) {
1907                    mapWrap.hidden = true;
1908                }
1909
1910                if (mapToggle) {
1911                    mapToggle.setAttribute('aria-expanded', 'false');
1912
1913                    const label =
1914                        mapToggle.querySelector('.cs-availability-search__map-toggle-label');
1915
1916                    if (label) {
1917                        label.textContent = 'Show Map';
1918                    }
1919                }
1920
1921                root.hidden = true;
1922                root.classList.remove('has-results');
1923
1924                try {
1925                    window.sessionStorage.removeItem(premiumStateKey);
1926                } catch (error) {
1927                    /* Storage is an enhancement only. */
1928                }
1929
1930                if (searchRoot) {
1931                    searchRoot.scrollIntoView({
1932                        behavior: 'smooth',
1933                        block: 'center'
1934                    });
1935                }
1936            }
1937        );
1938    }
1939
1940
1941    /* =====================================================
1942       Live AJAX filtering
1943       ===================================================== */
1944
1945    function bindLiveFiltering(
1946        root,
1947        form,
1948        state
1949    ) {
1950
1951        form.addEventListener(
1952            "submit",
1953            function (event) {
1954                event.preventDefault();
1955            }
1956        );
1957
1958        form.addEventListener(
1959            "change",
1960            function (event) {
1961
1962                const target =
1963                    event.target;
1964
1965                if (
1966                    !target
1967                    ||
1968                    !target.matches(
1969                        "select, input"
1970                    )
1971                ) {
1972                    return;
1973                }
1974
1975                queueFetch(
1976                    root,
1977                    form,
1978                    state
1979                );
1980            }
1981        );
1982
1983        form.addEventListener(
1984            "csAvailabilityClear",
1985            function () {
1986                queueFetch(
1987                    root,
1988                    form,
1989                    state,
1990                    0
1991                );
1992            }
1993        );
1994    }
1995
1996
1997    function queueFetch(
1998        root,
1999        form,
2000        state,
2001        delay
2002    ) {
2003
2004        clearTimeout(
2005            state.debounceTimer
2006        );
2007
2008        state.debounceTimer =
2009            setTimeout(
2010                function () {
2011                    fetchResults(
2012                        root,
2013                        form,
2014                        state
2015                    );
2016                },
2017                typeof delay === "number"
2018                    ? delay
2019                    : 280
2020            );
2021    }
2022
2023
2024    async function fetchResults(
2025        root,
2026        form,
2027        state
2028    ) {
2029
2030        const settings =
2031            window.CSAvailabilitySearch
2032            ||
2033            {};
2034
2035        if (
2036            !settings.ajaxUrl
2037            ||
2038            !settings.nonce
2039        ) {
2040            return;
2041        }
2042
2043        if (state.abortController) {
2044            state.abortController.abort();
2045        }
2046
2047        state.abortController =
2048            new AbortController();
2049
2050        const data =
2051            new FormData(form);
2052
2053        data.append(
2054            "action",
2055            "cornish_stays_availability_filter"
2056        );
2057
2058        data.append(
2059            "nonce",
2060            settings.nonce
2061        );
2062
2063        setLoading(
2064            root,
2065            true
2066        );
2067
2068        try {
2069
2070            const response =
2071                await fetch(
2072                    settings.ajaxUrl,
2073                    {
2074                        method: "POST",
2075                        credentials: "same-origin",
2076                        body: data,
2077                        signal:
2078                            state
2079                                .abortController
2080                                .signal
2081                    }
2082                );
2083
2084            const json =
2085                await response.json();
2086
2087            if (
2088                !json
2089                ||
2090                !json.success
2091                ||
2092                !json.data
2093            ) {
2094
2095                const message =
2096                    json
2097                    &&
2098                    json.data
2099                    &&
2100                    json.data.message
2101                        ? json.data.message
2102                        : "Could not update properties. Please try again.";
2103
2104                throw new Error(
2105                    message
2106                );
2107            }
2108
2109            updateResults(
2110                root,
2111                json.data
2112            );
2113
2114            state.markerData =
2115                Array.isArray(
2116                    json.data.markers
2117                )
2118                    ? json.data.markers
2119                    : [];
2120
2121            if (state.mapInitialised) {
2122                renderMarkers(
2123                    root,
2124                    state
2125                );
2126            }
2127
2128            bindCardInteractions(
2129                root,
2130                state
2131            );
2132
2133
2134        } catch (error) {
2135
2136            if (
2137                error.name ===
2138                "AbortError"
2139            ) {
2140                return;
2141            }
2142
2143            const results =
2144                root.querySelector(
2145                    "#cs-availability-results"
2146                );
2147
2148            if (results) {
2149                results.innerHTML =
2150                    '<div class="ps-empty">'
2151                    +
2152                    escapeHtml(
2153                        error.message
2154                        ||
2155                        "Could not update properties. Please try again."
2156                    )
2157                    +
2158                    "</div>";
2159            }
2160
2161        } finally {
2162
2163            setLoading(
2164                root,
2165                false
2166            );
2167        }
2168    }
2169
2170
2171    /* =========================================================
2172     * Match the existing Cornish Stays property-listing excerpt crop.
2173     * Crops .ps-excerpt elements to 40 words and marks each element
2174     * with data-excerpt-crop-initialised="true".
2175     * ========================================================= */
2176    function initPropertyExcerptCrop(scope) {
2177
2178        const container = scope || document;
2179        const excerpts = container.querySelectorAll(".ps-excerpt");
2180
2181        if (!excerpts.length) {
2182            return;
2183        }
2184
2185        excerpts.forEach(function (excerpt) {
2186
2187            if (excerpt.dataset.excerptCropInitialised === "true") {
2188                return;
2189            }
2190
2191            const fullText = excerpt.textContent.trim();
2192            const words = fullText.split(/\s+/);
2193
2194            if (words.length > 40) {
2195                excerpt.textContent =
2196                    words.slice(0, 40).join(" ")
2197                    +
2198                    "...";
2199            }
2200
2201            excerpt.dataset.excerptCropInitialised = "true";
2202        });
2203    }
2204
2205
2206    function updateResults(
2207        root,
2208        data
2209    ) {
2210
2211        const title =
2212            root.querySelector(
2213                "#cs-availability-results-title"
2214            );
2215
2216        const detail =
2217            root.querySelector(
2218                "#cs-availability-results-detail"
2219            );
2220
2221        const results =
2222            root.querySelector(
2223                "#cs-availability-results"
2224            );
2225
2226        if (title) {
2227            title.textContent =
2228                data.summaryTitle
2229                ||
2230                "";
2231        }
2232
2233        if (detail) {
2234            detail.textContent =
2235                data.summaryDetail
2236                ||
2237                "";
2238        }
2239
2240        if (results) {
2241            results.innerHTML =
2242                data.html
2243                ||
2244                "";
2245
2246            initPropertyExcerptCrop(results);
2247        }
2248
2249        updateAvailabilitySortState(
2250            root,
2251            Boolean(data.hasDates)
2252        );
2253
2254        applyAvailabilitySort(root);
2255    }
2256
2257
2258    /* =========================================================
2259     * Client-side result sorting.
2260     * Reorders the cards already returned by the availability search,
2261     * so changing the dropdown never makes another API/AJAX request.
2262     * ========================================================= */
2263    function bindAvailabilitySorting(root) {
2264
2265        const select =
2266            root.querySelector(
2267                "[data-availability-sort]"
2268            );
2269
2270        if (!select || select.dataset.sortBound === "1") {
2271            return;
2272        }
2273
2274        select.dataset.sortBound = "1";
2275
2276        select.addEventListener(
2277            "change",
2278            function () {
2279                applyAvailabilitySort(root);
2280            }
2281        );
2282    }
2283
2284
2285    function updateAvailabilitySortState(root, hasDates) {
2286
2287        const select =
2288            root.querySelector(
2289                "[data-availability-sort]"
2290            );
2291
2292        if (!select) {
2293            return;
2294        }
2295
2296        select
2297            .querySelectorAll(
2298                'option[data-requires-price="1"]'
2299            )
2300            .forEach(function (option) {
2301                option.disabled = !hasDates;
2302            });
2303
2304        if (
2305            !hasDates
2306            &&
2307            (
2308                select.value === "price-asc"
2309                ||
2310                select.value === "price-desc"
2311            )
2312        ) {
2313            select.value = "recommended";
2314        }
2315    }
2316
2317
2318    function applyAvailabilitySort(root) {
2319
2320        const select =
2321            root.querySelector(
2322                "[data-availability-sort]"
2323            );
2324
2325        const results =
2326            root.querySelector(
2327                "#cs-availability-results"
2328            );
2329
2330        if (!select || !results) {
2331            return;
2332        }
2333
2334        const sortMode = select.value || "recommended";
2335
2336        results
2337            .querySelectorAll(
2338                ".cs-availability-results-grid"
2339            )
2340            .forEach(function (grid) {
2341
2342                const cards =
2343                    Array.from(
2344                        grid.querySelectorAll(
2345                            ".cs-availability-card"
2346                        )
2347                    );
2348
2349                if (cards.length < 2) {
2350                    return;
2351                }
2352
2353                cards.forEach(function (card, index) {
2354                    if (!card.hasAttribute("data-sort-original")) {
2355                        card.setAttribute(
2356                            "data-sort-original",
2357                            String(index)
2358                        );
2359                    }
2360                });
2361
2362                cards.sort(function (a, b) {
2363
2364                    const originalA = Number(a.dataset.sortOriginal || 0);
2365                    const originalB = Number(b.dataset.sortOriginal || 0);
2366
2367                    if (sortMode === "recommended") {
2368                        return originalA - originalB;
2369                    }
2370
2371                    if (sortMode === "title-asc") {
2372                        return String(a.dataset.sortTitle || "")
2373                            .localeCompare(
2374                                String(b.dataset.sortTitle || ""),
2375                                undefined,
2376                                { sensitivity: "base" }
2377                            );
2378                    }
2379
2380                    if (
2381                        sortMode === "price-asc"
2382                        ||
2383                        sortMode === "price-desc"
2384                    ) {
2385                        const priceA = Number(a.dataset.sortPrice);
2386                        const priceB = Number(b.dataset.sortPrice);
2387                        const validA = Number.isFinite(priceA) && priceA > 0;
2388                        const validB = Number.isFinite(priceB) && priceB > 0;
2389
2390                        if (!validA && !validB) {
2391                            return originalA - originalB;
2392                        }
2393
2394                        if (!validA) {
2395                            return 1;
2396                        }
2397
2398                        if (!validB) {
2399                            return -1;
2400                        }
2401
2402                        return sortMode === "price-asc"
2403                            ? priceA - priceB
2404                            : priceB - priceA;
2405                    }
2406
2407                    if (
2408                        sortMode === "occupancy-asc"
2409                        ||
2410                        sortMode === "occupancy-desc"
2411                    ) {
2412                        const occupancyA = Number(a.dataset.sortOccupancy || 0);
2413                        const occupancyB = Number(b.dataset.sortOccupancy || 0);
2414
2415                        if (occupancyA === occupancyB) {
2416                            return originalA - originalB;
2417                        }
2418
2419                        return sortMode === "occupancy-asc"
2420                            ? occupancyA - occupancyB
2421                            : occupancyB - occupancyA;
2422                    }
2423
2424                    return originalA - originalB;
2425                });
2426
2427                cards.forEach(function (card) {
2428                    grid.appendChild(card);
2429                });
2430            });
2431    }
2432
2433
2434    function setLoading(
2435        root,
2436        loading
2437    ) {
2438
2439        const shell =
2440            root.querySelector(
2441                ".cs-availability-search__results-shell"
2442            );
2443
2444        const overlay =
2445            root.querySelector(
2446                ".cs-availability-search__loading"
2447            );
2448
2449        if (shell) {
2450            shell.setAttribute(
2451                "aria-busy",
2452                loading
2453                    ? "true"
2454                    : "false"
2455            );
2456        }
2457
2458        if (overlay) {
2459            overlay.hidden =
2460                !loading;
2461        }
2462    }
2463
2464
2465    function updateBrowserUrl(form) {
2466
2467        if (
2468            !window.history
2469            ||
2470            !window.history.replaceState
2471        ) {
2472            return;
2473        }
2474
2475        const params =
2476            new URLSearchParams();
2477
2478        new FormData(form)
2479            .forEach(
2480                function (value, key) {
2481
2482                    const stringValue =
2483                        String(value);
2484
2485                    const isDefault =
2486                        (key === "cs_area" && stringValue === "all")
2487                        ||
2488                        (key === "cs_town" && stringValue === "")
2489                        ||
2490                        (key === "cs_arrival" && stringValue === "")
2491                        ||
2492                        (key === "cs_departure" && stringValue === "")
2493                        ||
2494                        (key === "cs_date_flexibility" && stringValue === "0")
2495                        ||
2496                        (key === "cs_guests" && stringValue === "0");
2497
2498                    if (!isDefault) {
2499                        params.append(
2500                            key,
2501                            stringValue
2502                        );
2503                    }
2504                }
2505            );
2506
2507        const query =
2508            params.toString();
2509
2510        const nextUrl =
2511            window.location.pathname
2512            +
2513            (query ? "?" + query : "")
2514            +
2515            window.location.hash;
2516
2517        window.history.replaceState(
2518            {},
2519            "",
2520            nextUrl
2521        );
2522    }
2523
2524
2525    /* =====================================================
2526       Property card navigation
2527       ===================================================== */
2528
2529    function bindViewPropertyButtons(root) {
2530
2531        if (root.dataset.csViewPropertyBound === "true") {
2532            return;
2533        }
2534
2535        root.dataset.csViewPropertyBound = "true";
2536
2537        root.addEventListener(
2538            "click",
2539            function (event) {
2540
2541                const button =
2542                    event.target.closest(
2543                        ".cs-availability-card__view[data-view-property-url]"
2544                    );
2545
2546                if (!button || !root.contains(button)) {
2547                    return;
2548                }
2549
2550                const url =
2551                    button.getAttribute(
2552                        "data-view-property-url"
2553                    );
2554
2555                if (url) {
2556                    window.location.href = url;
2557                }
2558            }
2559        );
2560    }
2561
2562
2563    /* =====================================================
2564       SuperControl booking cart
2565       ===================================================== */
2566
2567    function bindBookingCart(
2568        root,
2569        form
2570    ) {
2571
2572        if (
2573            root.dataset
2574                .bookingCartBound
2575            ===
2576            "1"
2577        ) {
2578            return;
2579        }
2580
2581        root.dataset
2582            .bookingCartBound =
2583            "1";
2584
2585        root.addEventListener(
2586            "click",
2587            function (event) {
2588
2589                const button =
2590                    event.target.closest(
2591                        ".cs-availability-card__book"
2592                    );
2593
2594                if (
2595                    !button
2596                    ||
2597                    !root.contains(button)
2598                ) {
2599                    return;
2600                }
2601
2602                event.preventDefault();
2603
2604                const arrival =
2605                    button.getAttribute(
2606                        "data-booking-arrival"
2607                    )
2608                    ||
2609                    "";
2610
2611                const departure =
2612                    button.getAttribute(
2613                        "data-booking-departure"
2614                    )
2615                    ||
2616                    "";
2617
2618                const nights =
2619                    button.getAttribute(
2620                        "data-booking-nights"
2621                    )
2622                    ||
2623                    "";
2624
2625                const cottageID =
2626                    button.getAttribute(
2627                        "data-booking-cottage-id"
2628                    )
2629                    ||
2630                    "";
2631
2632                if (
2633                    !arrival
2634                    ||
2635                    !departure
2636                    ||
2637                    !nights
2638                    ||
2639                    !cottageID
2640                ) {
2641                    return;
2642                }
2643
2644                /*
2645                 * Anonymous first-party analytics. This is deliberately
2646                 * fire-and-forget so logging can never delay booking.
2647                 */
2648                try {
2649                    const settings = window.CSAvailabilitySearch || {};
2650                    const card = button.closest(".cs-availability-card");
2651                    const analyticsData = new FormData();
2652                    const parseScDate = function (value) {
2653                        const parts = String(value).split("/");
2654                        return parts.length === 3
2655                            ? parts[2] + "-" + parts[1] + "-" + parts[0]
2656                            : "";
2657                    };
2658
2659                    analyticsData.append("action", "cornish_stays_availability_log_book_click");
2660                    analyticsData.append("nonce", settings.nonce || "");
2661                    analyticsData.append("post_id", card ? (card.getAttribute("data-property-id") || "") : "");
2662                    analyticsData.append("property_id", cottageID);
2663                    analyticsData.append("arrival", parseScDate(arrival));
2664                    analyticsData.append("departure", parseScDate(departure));
2665                    analyticsData.append("price", card ? (card.getAttribute("data-sort-price") || "") : "");
2666
2667                    if (settings.ajaxUrl && settings.nonce) {
2668                        fetch(settings.ajaxUrl, {
2669                            method: "POST",
2670                            credentials: "same-origin",
2671                            body: analyticsData,
2672                            keepalive: true
2673                        }).catch(function () {});
2674                    }
2675                } catch (analyticsError) {
2676                    /* Analytics must never interrupt the booking flow. */
2677                }
2678
2679                const cartDetails = {
2680                    action: "cartOpen",
2681                    arrivalDate:
2682                        String(arrival),
2683                    departureDate:
2684                        String(departure),
2685                    numberNights:
2686                        String(nights),
2687                    cottageID:
2688                        String(cottageID)
2689                };
2690
2691                button.setAttribute(
2692                    "data-default-label",
2693                    button.textContent.trim()
2694                    ||
2695                    "Book Now"
2696                );
2697
2698                button.disabled = true;
2699                button.classList.add(
2700                    "is-booking"
2701                );
2702
2703                if (
2704                    CSAvailabilitySuperControlCart.ready
2705                ) {
2706
2707                    window.postMessage(
2708                        cartDetails,
2709                        "*"
2710                    );
2711
2712                    restoreBookingButton(
2713                        button
2714                    );
2715
2716                    return;
2717                }
2718
2719                button.textContent =
2720                    "Loading Booking…";
2721
2722                CSAvailabilitySuperControlCart.pending = {
2723                    details: cartDetails,
2724                    button: button
2725                };
2726
2727                clearTimeout(
2728                    CSAvailabilitySuperControlCart.pendingTimer
2729                );
2730
2731                CSAvailabilitySuperControlCart.pendingTimer =
2732                    setTimeout(
2733                        function () {
2734
2735                            const pending =
2736                                CSAvailabilitySuperControlCart.pending;
2737
2738                            CSAvailabilitySuperControlCart.pending =
2739                                null;
2740
2741                            if (
2742                                pending
2743                                &&
2744                                pending.button
2745                            ) {
2746                                restoreBookingButton(
2747                                    pending.button
2748                                );
2749                            }
2750                        },
2751                        10000
2752                    );
2753            }
2754        );
2755    }
2756
2757
2758    /* =====================================================
2759       Expandable map
2760       ===================================================== */
2761
2762    function bindMapToggle(
2763        root,
2764        state
2765    ) {
2766
2767        const button =
2768            root.querySelector(
2769                ".cs-availability-search__map-toggle"
2770            );
2771
2772        const wrap =
2773            root.querySelector(
2774                ".cs-availability-search__map-wrap"
2775            );
2776
2777        if (!button || !wrap) {
2778            return;
2779        }
2780
2781        button.addEventListener(
2782            "click",
2783            function () {
2784
2785                const opening =
2786                    button.getAttribute(
2787                        "aria-expanded"
2788                    )
2789                    !==
2790                    "true";
2791
2792                button.setAttribute(
2793                    "aria-expanded",
2794                    opening
2795                        ? "true"
2796                        : "false"
2797                );
2798
2799                wrap.hidden =
2800                    !opening;
2801
2802                const label =
2803                    button.querySelector(
2804                        ".cs-availability-search__map-toggle-label"
2805                    );
2806
2807                if (label) {
2808                    label.textContent =
2809                        opening
2810                            ? "Hide Map"
2811                            : "Show Map";
2812                }
2813
2814                if (!opening) {
2815                    return;
2816                }
2817
2818                if (!state.mapInitialised) {
2819                    initialiseMap(
2820                        root,
2821                        state
2822                    );
2823                } else if (state.map) {
2824                    setTimeout(
2825                        function () {
2826                            state.map.invalidateSize();
2827                            renderMarkers(
2828                                root,
2829                                state
2830                            );
2831                        },
2832                        60
2833                    );
2834                }
2835            }
2836        );
2837    }
2838
2839
2840    function readInitialMarkerData(root) {
2841
2842        const element =
2843            root.querySelector(
2844                "#cs-availability-map-data"
2845            );
2846
2847        if (!element) {
2848            return [];
2849        }
2850
2851        try {
2852            return JSON.parse(
2853                element.textContent
2854                ||
2855                "[]"
2856            );
2857        } catch (error) {
2858            return [];
2859        }
2860    }
2861
2862
2863    function initialiseMap(
2864        root,
2865        state
2866    ) {
2867
2868        const mapElement =
2869            root.querySelector(
2870                ".cs-availability-search__map"
2871            );
2872
2873        if (
2874            !mapElement
2875            ||
2876            typeof L === "undefined"
2877        ) {
2878            return;
2879        }
2880
2881        const config =
2882            (
2883                window.CSAvailabilitySearch
2884                &&
2885                CSAvailabilitySearch.map
2886            )
2887                ? CSAvailabilitySearch.map
2888                : {
2889                    defaultLat: 50.266,
2890                    defaultLng: -5.0527,
2891                    defaultZoom: 9
2892                };
2893
2894        state.map =
2895            L.map(
2896                mapElement,
2897                {
2898                    scrollWheelZoom: false
2899                }
2900            ).setView(
2901                [
2902                    Number(
2903                        config.defaultLat
2904                    ),
2905                    Number(
2906                        config.defaultLng
2907                    )
2908                ],
2909                Number(
2910                    config.defaultZoom
2911                )
2912            );
2913
2914        L.tileLayer(
2915            "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
2916            {
2917                maxZoom: 19,
2918                attribution:
2919                    "&copy; OpenStreetMap contributors"
2920            }
2921        ).addTo(
2922            state.map
2923        );
2924
2925        state.markerLayer =
2926            L.layerGroup()
2927                .addTo(
2928                    state.map
2929                );
2930
2931        state.mapInitialised =
2932            true;
2933
2934        renderMarkers(
2935            root,
2936            state
2937        );
2938
2939        setTimeout(
2940            function () {
2941                state.map.invalidateSize();
2942            },
2943            80
2944        );
2945    }
2946
2947
2948    function renderMarkers(
2949        root,
2950        state
2951    ) {
2952
2953        if (
2954            !state.map
2955            ||
2956            !state.markerLayer
2957        ) {
2958            return;
2959        }
2960
2961        state.markerLayer.clearLayers();
2962        state.markersByPostId.clear();
2963
2964        const bounds = [];
2965
2966        state.markerData.forEach(
2967            function (markerData) {
2968
2969                const lat =
2970                    Number(
2971                        markerData.lat
2972                    );
2973
2974                const lng =
2975                    Number(
2976                        markerData.lng
2977                    );
2978
2979                if (
2980                    !Number.isFinite(lat)
2981                    ||
2982                    !Number.isFinite(lng)
2983                ) {
2984                    return;
2985                }
2986
2987                const marker =
2988                    L.marker(
2989                        [lat, lng]
2990                    )
2991                    .addTo(
2992                        state.markerLayer
2993                    )
2994                    .bindPopup(
2995                        markerData.popupHtml
2996                        ||
2997                        "",
2998                        {
2999                            maxWidth: 270
3000                        }
3001                    );
3002
3003                const propertyId =
3004                    String(
3005                        markerData.postId
3006                    );
3007
3008                state.markersByPostId.set(
3009                    propertyId,
3010                    marker
3011                );
3012
3013                bounds.push(
3014                    [lat, lng]
3015                );
3016            }
3017        );
3018
3019        if (bounds.length) {
3020
3021            state.map.fitBounds(
3022                bounds,
3023                {
3024                    padding: [35, 35],
3025                    maxZoom: 13
3026                }
3027            );
3028
3029        } else {
3030
3031            const config =
3032                CSAvailabilitySearch.map;
3033
3034            state.map.setView(
3035                [
3036                    Number(
3037                        config.defaultLat
3038                    ),
3039                    Number(
3040                        config.defaultLng
3041                    )
3042                ],
3043                Number(
3044                    config.defaultZoom
3045                )
3046            );
3047        }
3048    }
3049
3050
3051    /* =====================================================
3052       Card -> map interaction
3053       ===================================================== */
3054
3055    function bindCardInteractions(
3056        root,
3057        state
3058    ) {
3059
3060        root
3061            .querySelectorAll(
3062                ".cs-availability-card[data-property-id]"
3063            )
3064            .forEach(
3065                function (card) {
3066
3067                    if (
3068                        card.dataset
3069                            .mapInteractionBound
3070                        ===
3071                        "1"
3072                    ) {
3073                        return;
3074                    }
3075
3076                    card.dataset
3077                        .mapInteractionBound =
3078                        "1";
3079
3080                    const propertyId =
3081                        String(
3082                            card.getAttribute(
3083                                "data-property-id"
3084                            )
3085                        );
3086
3087                    card.addEventListener(
3088                        "mouseenter",
3089                        function () {
3090
3091                            if (
3092                                !state.mapInitialised
3093                                ||
3094                                !state.map
3095                            ) {
3096                                return;
3097                            }
3098
3099                            const marker =
3100                                state
3101                                    .markersByPostId
3102                                    .get(
3103                                        propertyId
3104                                    );
3105
3106                            if (!marker) {
3107                                return;
3108                            }
3109
3110                            root
3111                                .querySelectorAll(
3112                                    ".cs-availability-card.is-map-active"
3113                                )
3114                                .forEach(
3115                                    function (item) {
3116                                        item.classList.remove(
3117                                            "is-map-active"
3118                                        );
3119                                    }
3120                                );
3121
3122                            state
3123                                .markersByPostId
3124                                .forEach(
3125                                    function (item) {
3126
3127                                        const icon =
3128                                            item.getElement();
3129
3130                                        if (icon) {
3131                                            icon.classList.remove(
3132                                                "is-highlighted"
3133                                            );
3134                                        }
3135                                    }
3136                                );
3137
3138                            card.classList.add(
3139                                "is-map-active"
3140                            );
3141
3142                            const icon =
3143                                marker.getElement();
3144
3145                            if (icon) {
3146                                icon.classList.add(
3147                                    "is-highlighted"
3148                                );
3149                            }
3150
3151                            state.map.flyTo(
3152                                marker.getLatLng(),
3153                                Math.max(
3154                                    state.map.getZoom(),
3155                                    14
3156                                ),
3157                                {
3158                                    animate: true,
3159                                    duration: .4
3160                                }
3161                            );
3162
3163                            marker.openPopup();
3164                        }
3165                    );
3166
3167                    card.addEventListener(
3168                        "mouseleave",
3169                        function () {
3170
3171                            card.classList.remove(
3172                                "is-map-active"
3173                            );
3174
3175                            const marker =
3176                                state
3177                                    .markersByPostId
3178                                    .get(
3179                                        propertyId
3180                                    );
3181
3182                            if (marker) {
3183                                const icon =
3184                                    marker.getElement();
3185
3186                                if (icon) {
3187                                    icon.classList.remove(
3188                                        "is-highlighted"
3189                                    );
3190                                }
3191                            }
3192                        }
3193                    );
3194                }
3195            );
3196    }
3197
3198
3199    /* =====================================================
3200       Utilities
3201       ===================================================== */
3202
3203    function escapeHtml(string) {
3204
3205        return String(
3206            string
3207        ).replace(
3208            /[&<>"']/g,
3209            function (character) {
3210
3211                return {
3212                    "&": "&amp;",
3213                    "<": "&lt;",
3214                    ">": "&gt;",
3215                    '"': "&quot;",
3216                    "'": "&#039;"
3217                }[character];
3218            }
3219        );
3220    }
3221}
3222
3223/*
3224 * Divi and optimisation plugins can alter script timing. Run once immediately,
3225 * then retry at the common lifecycle points. The per-root data guard above
3226 * prevents duplicate listeners while allowing late-rendered shortcode markup
3227 * to initialise reliably.
3228 */
3229initialiseCSAvailabilitySearch();
3230
3231if (document.readyState === "loading") {
3232    document.addEventListener(
3233        "DOMContentLoaded",
3234        initialiseCSAvailabilitySearch
3235    );
3236}
3237
3238window.addEventListener(
3239    "load",
3240    initialiseCSAvailabilitySearch
3241);
3242
3243window.addEventListener(
3244    "pageshow",
3245    initialiseCSAvailabilitySearch
3246);
3247
3248setTimeout(initialiseCSAvailabilitySearch, 0);
3249setTimeout(initialiseCSAvailabilitySearch, 250);
3250setTimeout(initialiseCSAvailabilitySearch, 1000);
3251
3252if (typeof MutationObserver !== "undefined") {
3253    const csAvailabilityObserver = new MutationObserver(function () {
3254        initialiseCSAvailabilitySearch();
3255    });
3256
3257    csAvailabilityObserver.observe(
3258        document.documentElement,
3259        { childList: true, subtree: true }
3260    );
3261
3262    window.setTimeout(function () {
3263        csAvailabilityObserver.disconnect();
3264    }, 5000);
3265}

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.