PageSourceSearch

https://mountaincabinsutah.com/wp-content/plugins/homerunner-toolkit/assets/js/guest-picker.js?ver=3.309.18

js mountaincabinsutah.com collected 2026-10-10 02:02:15 UTC 44,960 bytes, 1,183 lines download raw bytes

1/**
2 * Guest Picker Module — Frontend JS
3 *
4 * Replaces the booking widget's <select id="guests"> dropdown with
5 * a modal containing Adults / Children / Pets stepper controls.
6 * Injects adults, children, pets params into quote AJAX requests.
7 *
8 * Each picker is also the booking widget's party provider: on the core
9 * booking API (jQuery(widget).data("homelocalBookingApi"), announced by
10 * "homelocal:booking-widget:ready") it registers { get, validate, set }, so a
11 * stay set through that API carries the adults, children and pets the picker
12 * shows. window.hrtkGuestPicker = { ready, get, set } is the same state for
13 * other Toolkit scripts.
14 */
15(function ($) {
16  "use strict";
17
18  // State map: property_id -> { adults, children, pets }
19  var stateMap = {};
20
21  // i18n strings (passed from PHP via wp_localize_script).
22  var i18n = window.hrtkGPi18n || {};
23
24  // Party providers: "w:<widget id>" and "p:<property id>" -> { get, validate, set }.
25  var pickers = {};
26
27  // Resolves once the pickers on this page are initialised, or once the
28  // bootstrap below gave up looking for a widget. Null without Promise.
29  var readyDone = false;
30  var resolveReady = null;
31  var readyPromise =
32    typeof window.Promise === "function"
33      ? new window.Promise(function (resolve) {
34          resolveReady = resolve;
35        })
36      : null;
37
38  function markReady() {
39    if (readyDone) return;
40    readyDone = true;
41
42    var count = 0;
43    for (var key in pickers) {
44      if (pickers.hasOwnProperty(key) && key.indexOf("w:") === 0) count++;
45    }
46
47    if (resolveReady) resolveReady({ widgets: count });
48
49    // For a script that loaded before this one and could not read `ready`.
50    if (typeof window.CustomEvent === "function") {
51      document.dispatchEvent(
52        new CustomEvent("hrtk:guest-picker:ready", { detail: { widgets: count } })
53      );
54    }
55  }
56
57  function wholeNumber(value) {
58    if (typeof value === "string" && /^\d+$/.test(value)) value = Number(value);
59    return typeof value === "number" && isFinite(value) && Math.floor(value) === value && value >= 0
60      ? value
61      : null;
62  }
63
64  /**
65   * Checks a party against one widget's limits, the same limits the steppers
66   * enforce. Returns { ok, reason, reason_text, party }: `party` is the party
67   * as whole numbers when ok, `reason` one of party_unsupported,
68   * party_over_capacity, pets_not_allowed when not.
69   */
70  function validateParty(party, limits) {
71    party = party || {};
72
73    var adults = wholeNumber(party.adults);
74    var children = wholeNumber(party.children === undefined || party.children === null ? 0 : party.children);
75    var pets = wholeNumber(party.pets === undefined || party.pets === null ? 0 : party.pets);
76
77    function refuse(reason, text) {
78      return { ok: false, reason: reason, reason_text: text, party: null };
79    }
80
81    if (adults === null || adults < 1 || children === null || pets === null) {
82      return refuse(
83        "party_unsupported",
84        i18n.partyInvalid || "Guests are whole numbers, with at least one adult."
85      );
86    }
87
88    if (limits.maxAdults > 0 && adults > limits.maxAdults) {
89      return refuse(
90        "party_over_capacity",
91        limits.maxAdults === 1
92          ? (i18n.maxAdultsSingular || "This property allows a maximum of 1 adult")
93          : (i18n.maxAdultsPlural || "This property allows a maximum of %d adults").replace("%d", limits.maxAdults)
94      );
95    }
96
97    if (adults + children > limits.maxGuests) {
98      return refuse(
99        "party_over_capacity",
100        (i18n.maxGuestsPlural || "This property allows a maximum of %d guests").replace("%d", limits.maxGuests)
101      );
102    }
103
104    if (pets > 0 && !limits.petsAllowed) {
105      return refuse("pets_not_allowed", i18n.noPets || "This property does not allow pets");
106    }
107
108    if (limits.maxPets > 0 && pets > limits.maxPets) {
109      return refuse(
110        "party_over_capacity",
111        limits.maxPets === 1
112          ? (i18n.maxPetsSingular || "This property allows a maximum of 1 pet")
113          : (i18n.maxPetsPlural || "This property allows a maximum of %d pets").replace("%d", limits.maxPets)
114      );
115    }
116
117    return {
118      ok: true,
119      reason: null,
120      reason_text: null,
121      party: { adults: adults, children: children, pets: pets },
122    };
123  }
124
125  function findPicker(key) {
126    return pickers["w:" + key] || pickers["p:" + key] || null;
127  }
128
129  window.hrtkGuestPicker = {
130    ready: readyPromise,
131
132    // The party a picker shows, by widget id or property id; null without one.
133    get: function (widgetIdOrPropertyId) {
134      var picker = findPicker(widgetIdOrPropertyId);
135      return picker ? picker.get() : null;
136    },
137
138    // Puts a party into a picker without starting a quote. Returns the verdict.
139    set: function (widgetIdOrPropertyId, party) {
140      var picker = findPicker(widgetIdOrPropertyId);
141      if (!picker) {
142        return { ok: false, reason: "party_unsupported", reason_text: null, party: null };
143      }
144      return picker.set(party);
145    },
146  };
147
148  // ── Utility ────────────────────────────────────────────────────
149
150  function formatSummary(state) {
151    var parts = [];
152    parts.push(state.adults + " " + (state.adults === 1 ? (i18n.adult || "Adult") : (i18n.adultsPlural || "Adults")));
153    if (state.children > 0) {
154      parts.push(state.children + " " + (state.children === 1 ? (i18n.child || "Child") : (i18n.childrenPlural || "Children")));
155    }
156    if (state.pets > 0) {
157      parts.push(state.pets + " " + (state.pets === 1 ? (i18n.pet || "Pet") : (i18n.petsPlural || "Pets")));
158    }
159    return parts.join(", ");
160  }
161
162  function getUrlParam(name) {
163    var params = new URLSearchParams(window.location.search);
164    return params.get(name);
165  }
166
167  // ── Modal HTML builder ─────────────────────────────────────────
168
169  function buildModal(id, petsAllowed, maxPets) {
170    var petsClass = petsAllowed ? "" : " hrtk-gp-pets-disabled";
171    var petsNotice = petsAllowed
172      ? ""
173      : '<div class="hrtk-gp-pets-notice">' + (i18n.noPets || "This property does not allow pets") + '</div>';
174
175    return (
176      '<div id="hrtk-gp-modal-' + id + '" class="hrtk-gp-modal hk-modal" hk-modal>' +
177        '<div class="hk-modal-dialog hk-margin-auto-vertical hk-flex hk-flex-column">' +
178          '<button class="hk-modal-close-default" type="button" hk-close></button>' +
179          '<div class="hk-modal-header">' +
180            '<span class="hk-h4">' +
181              '<span class="hk-margin-small-right hk-icon" hk-icon="icon: user"></span>' +
182              (i18n.guests || "Guests") +
183            "</span>" +
184          "</div>" +
185          '<div class="hk-modal-body">' +
186            // Adults
187            '<div class="hrtk-gp-row-item">' +
188              '<div class="hrtk-gp-row-label">' +
189                '<span class="hrtk-gp-label">' + (i18n.adults || "Adults") + '</span>' +
190              "</div>" +
191              '<div class="hrtk-gp-stepper" data-field="adults">' +
192                '<button type="button" class="hrtk-gp-stepper-btn hrtk-gp-minus disabled" data-field="adults">&minus;</button>' +
193                '<span class="hrtk-gp-stepper-value" data-field="adults">1</span>' +
194                '<button type="button" class="hrtk-gp-stepper-btn hrtk-gp-plus" data-field="adults">+</button>' +
195              "</div>" +
196            "</div>" +
197            // Children
198            '<div class="hrtk-gp-row-item">' +
199              '<div class="hrtk-gp-row-label">' +
200                '<span class="hrtk-gp-label">' + (i18n.children || "Children") + '</span>' +
201              "</div>" +
202              '<div class="hrtk-gp-stepper" data-field="children">' +
203                '<button type="button" class="hrtk-gp-stepper-btn hrtk-gp-minus disabled" data-field="children">&minus;</button>' +
204                '<span class="hrtk-gp-stepper-value" data-field="children">0</span>' +
205                '<button type="button" class="hrtk-gp-stepper-btn hrtk-gp-plus" data-field="children">+</button>' +
206              "</div>" +
207            "</div>" +
208            // Pets
209            '<div class="hrtk-gp-row-item hrtk-gp-pets-row' + petsClass + '">' +
210              '<div class="hrtk-gp-row-label">' +
211                '<span class="hrtk-gp-label">' + (i18n.pets || "Pets") + '</span>' +
212                petsNotice +
213              "</div>" +
214              '<div class="hrtk-gp-stepper" data-field="pets">' +
215                '<button type="button" class="hrtk-gp-stepper-btn hrtk-gp-minus disabled" data-field="pets">&minus;</button>' +
216                '<span class="hrtk-gp-stepper-value" data-field="pets">0</span>' +
217                '<div class="hrtk-gp-plus-wrap">' +
218                  '<button type="button" class="hrtk-gp-stepper-btn hrtk-gp-plus" data-field="pets">+</button>' +
219                  '<div class="hrtk-gp-tooltip" style="display:none"></div>' +
220                '</div>' +
221              "</div>" +
222            "</div>" +
223            '<div class="hrtk-gp-pet-notice" style="display:none"></div>' +
224          "</div>" +
225          '<div class="hk-modal-footer">' +
226            '<button type="button" class="hrtk-gp-reset hk-hidden">' + (i18n.clear || "Clear") + '</button>' +
227            '<button type="button" class="hk-button hk-button-small hk-button-primary hk-margin-auto-left hrtk-gp-apply">' + (i18n.apply || "Apply") + '</button>' +
228          "</div>" +
229        "</div>" +
230      "</div>"
231    );
232  }
233
234  // ── Per-widget initializer ─────────────────────────────────────
235
236  function initGuestPicker(shortcode) {
237    var $widget = $(shortcode.selector);
238    if (!$widget.length) return;
239
240    var $dataEl = $widget.find(".hrtk-gp-data");
241    if (!$dataEl.length) return;
242
243    var petsAllowed = $dataEl.data("pets-allowed") === 1 || $dataEl.data("pets-allowed") === "1";
244    var maxGuests = parseInt($dataEl.data("max-guests"), 10) || 20;
245    // PMS adult cap (OwnerRez max_adults via Central additional_info); 0 = none.
246    var maxAdults = parseInt($dataEl.data("max-adults"), 10) || 0;
247    var maxPets = parseInt($dataEl.data("max-pets"), 10) || 0; // 0 = no known limit
248    var petNoticeEnabled = $dataEl.data("pet-notice-enabled") === 1 || $dataEl.data("pet-notice-enabled") === "1";
249    var petNoticeText = $dataEl.data("pet-notice-text") || "";
250    var propertyId = shortcode.config.property_id;
251    var widgetId =
252      $widget.attr("id") || "bw-" + Math.random().toString(36).substr(2, 6);
253
254    // Initialize state.
255    var state = {
256      adults: 1,
257      children: 0,
258      pets: 0,
259    };
260
261    // Pre-populate from URL params if present.
262    var urlAdults = getUrlParam("adults");
263    var urlChildren = getUrlParam("children");
264    var urlPets = getUrlParam("pets");
265
266    if (urlAdults) state.adults = Math.max(1, parseInt(urlAdults, 10) || 1);
267    if (urlChildren) state.children = Math.max(0, parseInt(urlChildren, 10) || 0);
268    if (urlPets && petsAllowed) {
269      if (urlPets === "yes") {
270        state.pets = 1;
271      } else if (urlPets !== "no") {
272        state.pets = Math.max(0, parseInt(urlPets, 10) || 0);
273      }
274    }
275
276    // Enforce max adults constraint before the total, so a URL with too many
277    // adults is trimmed to what the PMS will quote.
278    if (maxAdults > 0 && state.adults > maxAdults) {
279      state.adults = maxAdults;
280    }
281
282    // Enforce max guests constraint.
283    if (state.adults + state.children > maxGuests) {
284      state.children = Math.max(0, maxGuests - state.adults);
285    }
286
287    // Enforce max pets constraint.
288    if (maxPets > 0 && state.pets > maxPets) {
289      state.pets = maxPets;
290    }
291
292    // The applied party: what the summary, the URL and every quote request
293    // carry. `state` above is the modal's draft: the steppers change it, and
294    // Apply copies it here. The two differ only while the guest has stepper
295    // changes they have not applied.
296    var committed = { adults: state.adults, children: state.children, pets: state.pets };
297
298    // Has the guest changed the draft? Only then does closing the modal
299    // apply it. An untouched draft never reprices: the modal also closes
300    // when another one opens (the choice of checkout), and that must not
301    // replace the quote the guest is about to check out.
302    function draftChanged() {
303      return (
304        state.adults !== committed.adults ||
305        state.children !== committed.children ||
306        state.pets !== committed.pets
307      );
308    }
309
310    // Store in global state map.
311    stateMap[propertyId] = committed;
312
313    // ── Hide original dropdown ─────────────────────────────────
314    var $guestsRow = $widget.find(".guests-row");
315    $guestsRow.hide();
316
317    // ── Inject trigger button ──────────────────────────────────
318    var $triggerRow = $(
319      '<div class="hk-width-1-1 hrtk-gp-row">' +
320        '<label class="hk-form-label">' + (i18n.guests || "Guests") + '</label>' +
321        '<div class="hk-margin-small hk-form-controls">' +
322          '<button type="button" class="hrtk-gp-trigger">' +
323            '<span class="hrtk-gp-summary">' +
324              formatSummary(state) +
325            "</span>" +
326          "</button>" +
327        "</div>" +
328      "</div>"
329    );
330    $guestsRow.after($triggerRow);
331
332    // ── Inject modal ───────────────────────────────────────────
333    var modalId = "hrtk-gp-modal-" + widgetId;
334    var $modal = $(buildModal(widgetId, petsAllowed, maxPets));
335
336    // Append to the closest modal scope or body.
337    var $scope = $widget.closest(".hk-scope-modal");
338    if ($scope.length) {
339      $scope.append($modal);
340    } else {
341      $("body").append($modal);
342    }
343
344    var $summary = $triggerRow.find(".hrtk-gp-summary");
345    var $tooltip = $modal.find(".hrtk-gp-tooltip");
346    var tooltipTimer = null;
347
348    // ── UI references inside modal ─────────────────────────────
349    function getValueEl(field) {
350      return $modal.find('.hrtk-gp-stepper-value[data-field="' + field + '"]');
351    }
352    function getMinusBtn(field) {
353      return $modal.find('.hrtk-gp-minus[data-field="' + field + '"]');
354    }
355    function getPlusBtn(field) {
356      return $modal.find('.hrtk-gp-plus[data-field="' + field + '"]');
357    }
358
359    // ── Render state into modal ────────────────────────────────
360    function renderState() {
361      var totalGuests = state.adults + state.children;
362
363      getValueEl("adults").text(state.adults);
364      getValueEl("children").text(state.children);
365      getValueEl("pets").text(state.pets);
366
367      // Adults minus: disabled at 1.
368      getMinusBtn("adults").toggleClass("disabled", state.adults <= 1);
369      // Adults plus: disabled at the total cap or the PMS adult cap.
370      var adultsAtMax = maxAdults > 0 && state.adults >= maxAdults;
371      getPlusBtn("adults").toggleClass("disabled", totalGuests >= maxGuests || adultsAtMax);
372
373      // Children minus: disabled at 0.
374      getMinusBtn("children").toggleClass("disabled", state.children <= 0);
375      // Children plus: disabled at max.
376      getPlusBtn("children").toggleClass("disabled", totalGuests >= maxGuests);
377
378      // Pets: handle both not-allowed and max-pets cases.
379      if (petsAllowed) {
380        getMinusBtn("pets").toggleClass("disabled", state.pets <= 0);
381        var petsAtMax = maxPets > 0 && state.pets >= maxPets;
382        getPlusBtn("pets").toggleClass("disabled", petsAtMax);
383      }
384
385      // Pet notice visibility.
386      if (petNoticeEnabled && petNoticeText) {
387        var $petNotice = $modal.find(".hrtk-gp-pet-notice");
388        if (state.pets > 0) {
389          $petNotice.text(petNoticeText).show();
390        } else {
391          $petNotice.hide();
392        }
393      }
394
395      // Clear button visibility.
396      var isDefault = state.adults === 1 && state.children === 0 && state.pets === 0;
397      $modal.find(".hrtk-gp-reset").toggleClass("hk-hidden", isDefault);
398    }
399
400    // ── Show tooltip on disabled pets + button ─────────────────
401    function showPetsTooltip() {
402      if (!maxPets || maxPets <= 0) return;
403
404      var msg = maxPets === 1
405        ? (i18n.maxPetsSingular || "This property allows a maximum of 1 pet")
406        : (i18n.maxPetsPlural || "This property allows a maximum of %d pets").replace("%d", maxPets);
407
408      $tooltip.text(msg).fadeIn(150);
409
410      clearTimeout(tooltipTimer);
411      tooltipTimer = setTimeout(function () {
412        $tooltip.fadeOut(150);
413      }, 3000);
414    }
415
416    // ── Show tooltip on capped adults + button ─────────────────
417    function showAdultsTooltip() {
418      if (!maxAdults || maxAdults <= 0) return;
419
420      var msg = maxAdults === 1
421        ? (i18n.maxAdultsSingular || "This property allows a maximum of 1 adult")
422        : (i18n.maxAdultsPlural || "This property allows a maximum of %d adults").replace("%d", maxAdults);
423
424      $tooltip.text(msg).fadeIn(150);
425
426      clearTimeout(tooltipTimer);
427      tooltipTimer = setTimeout(function () {
428        $tooltip.fadeOut(150);
429      }, 3000);
430    }
431
432    // ── Stepper click handlers ─────────────────────────────────
433    $modal.on("click", ".hrtk-gp-minus:not(.disabled)", function () {
434      var field = $(this).data("field");
435      var minVal = field === "adults" ? 1 : 0;
436
437      if (state[field] > minVal) {
438        state[field]--;
439        renderState();
440      }
441    });
442
443    $modal.on("click", ".hrtk-gp-plus", function () {
444      var field = $(this).data("field");
445      var $btn = $(this);
446      var totalGuests = state.adults + state.children;
447
448      if (field === "pets") {
449        if (!petsAllowed) return;
450
451        // Check max pets limit.
452        if (maxPets > 0 && state.pets >= maxPets) {
453          showPetsTooltip();
454          return;
455        }
456
457        state.pets++;
458        renderState();
459        return;
460      }
461
462      // Adults also stop at the PMS adult cap; explain why on click.
463      if (field === "adults" && maxAdults > 0 && state.adults >= maxAdults) {
464        showAdultsTooltip();
465        return;
466      }
467
468      // Adults and children share the max_guests cap.
469      if ($btn.hasClass("disabled")) return;
470      if (totalGuests < maxGuests) {
471        state[field]++;
472        renderState();
473      }
474    });
475
476    // ── Apply logic (shared by button and modal close) ─────────
477    //
478    // `announce` fires the "change" that makes the booking widget price the
479    // stay. The party provider commits without it: the booking API that
480    // called it runs the one quote itself.
481    function commitState(announce) {
482      // Everything below shows the applied party, never the modal's draft.
483
484      // Update summary text.
485      $summary.text(formatSummary(committed));
486
487      // Persist to sessionStorage for checkout page.
488      try {
489        sessionStorage.setItem("hrtk_guest_state", JSON.stringify(committed));
490      } catch (e) {}
491
492      // Sync the hidden #guests select for backward compat.
493      var totalGuests = committed.adults + committed.children;
494      var $guestsSelect = $widget.find("#guests");
495      if ($guestsSelect.length) {
496        if ($guestsSelect.find('option[value="' + totalGuests + '"]').length) {
497          $guestsSelect.val(totalGuests);
498        } else {
499          var lastVal = $guestsSelect.find("option:last").val();
500          $guestsSelect.val(lastVal);
501        }
502      }
503
504      // Update state map.
505      stateMap[propertyId] = committed;
506
507      // Trigger the change event to fire updateBookingCost.
508      if (announce) {
509        $guestsSelect.trigger("change");
510      }
511
512      // Update browser URL with guest details (replaceState, no history entry).
513      var url = new URL(window.location.href);
514      if (committed.adults > 1) {
515        url.searchParams.set("adults", committed.adults);
516      } else {
517        url.searchParams.delete("adults");
518      }
519      if (committed.children > 0) {
520        url.searchParams.set("children", committed.children);
521      } else {
522        url.searchParams.delete("children");
523      }
524      if (committed.pets > 0) {
525        url.searchParams.set("pets", committed.pets);
526      } else {
527        url.searchParams.delete("pets");
528      }
529      window.history.replaceState({}, "", url);
530    }
531
532    // The guest applies the draft.
533    function applyState() {
534      committed.adults = state.adults;
535      committed.children = state.children;
536      committed.pets = state.pets;
537      commitState(true);
538    }
539
540    // ── Apply button ───────────────────────────────────────────
541    $modal.on("click", ".hrtk-gp-apply", function () {
542      applyState();
543      closeModal();
544    });
545
546    // ── Modal close (click outside / X button) = auto-apply ────
547    // Only what the guest changed. After Apply, or with an untouched
548    // picker, there is nothing to apply and nothing is priced again.
549    $modal.on("hidden", function () {
550      if (draftChanged()) {
551        applyState();
552      }
553    });
554
555    // ── Clear button ───────────────────────────────────────────
556    $modal.on("click", ".hrtk-gp-reset", function () {
557      state.adults = 1;
558      state.children = 0;
559      state.pets = 0;
560      renderState();
561    });
562
563    // ── Open modal ─────────────────────────────────────────────
564    $triggerRow.on("click", ".hrtk-gp-trigger", function (e) {
565      e.preventDefault();
566      renderState();
567
568      var uikit = window.hkUIkit || window.UIkit;
569      if (uikit && uikit.modal) {
570        uikit.modal("#" + modalId).show();
571      }
572    });
573
574    function closeModal() {
575      var uikit = window.hkUIkit || window.UIkit;
576      if (uikit && uikit.modal) {
577        uikit.modal("#" + modalId).hide();
578      }
579    }
580
581    // ── Sync initial #guests value ─────────────────────────────
582    var $guestsSelect = $widget.find("#guests");
583    if ($guestsSelect.length) {
584      var initialTotal = state.adults + state.children;
585      if ($guestsSelect.find('option[value="' + initialTotal + '"]').length) {
586        $guestsSelect.val(initialTotal);
587      } else {
588        // If exact total exceeds max option, clamp to highest available.
589        var lastVal = $guestsSelect.find("option:last").val();
590        $guestsSelect.val(lastVal);
591      }
592
593      // When URL params drove the initial state, trigger change so any
594      // auto-quote on page load uses the correct guest breakdown.
595      if (urlAdults || urlChildren || urlPets) {
596        $guestsSelect.trigger("change");
597      }
598    }
599
600    // Persist initial URL-driven state to sessionStorage for checkout.
601    if (urlAdults || urlChildren || urlPets) {
602      try {
603        sessionStorage.setItem("hrtk_guest_state", JSON.stringify(state));
604      } catch (e) {}
605    }
606
607    renderState();
608
609    // ── Party provider for the core booking API ────────────────
610    var limits = {
611      petsAllowed: petsAllowed,
612      maxGuests: maxGuests,
613      maxAdults: maxAdults,
614      maxPets: maxPets,
615    };
616
617    // The provider reads and writes the applied party. A draft the guest has
618    // not applied is never reported as the party. A party set from here is
619    // the party in force: the steppers take it, open modal or not, so the
620    // picker shows what is priced and closing it applies nothing.
621    var provider = {
622      get: function () {
623        return { adults: committed.adults, children: committed.children, pets: committed.pets };
624      },
625
626      validate: function (party) {
627        return validateParty(party, limits);
628      },
629
630      // State, steppers, summary, the hidden guests value and the URL, with
631      // no "change": a change here would start a second quote and supersede
632      // the operation that is setting the stay.
633      set: function (party) {
634        var verdict = validateParty(party, limits);
635        if (!verdict.ok) return verdict;
636
637        committed.adults = verdict.party.adults;
638        committed.children = verdict.party.children;
639        committed.pets = verdict.party.pets;
640
641        // The draft follows, so it has no change of the guest's left in it.
642        // What the guest steps after this is theirs again.
643        state.adults = committed.adults;
644        state.children = committed.children;
645        state.pets = committed.pets;
646        renderState();
647
648        commitState(false);
649
650        return verdict;
651      },
652    };
653
654    pickers["w:" + widgetId] = provider;
655    pickers["p:" + propertyId] = provider;
656
657    function registerProvider() {
658      var api = $widget.data("homelocalBookingApi");
659      if (!api || typeof api.registerPartyProvider !== "function") return false;
660      return api.registerPartyProvider(provider) !== false;
661    }
662
663    // The widget may mount before or after this picker: register now when
664    // its API is there, and again whenever it announces itself.
665    registerProvider();
666    document.addEventListener("homelocal:booking-widget:ready", function (event) {
667      var detail = (event && event.detail) || {};
668      var mine = detail.widget_id
669        ? detail.widget_id === $widget.attr("id")
670        : String(detail.property_id) === String(propertyId);
671      if (mine) registerProvider();
672    });
673  }
674
675  // ── AJAX prefilter: inject adults/children/pets ───────────────
676
677  $.ajaxPrefilter(function (options) {
678    if (!options.url) return;
679    if (options.type !== "POST" && options.type !== "post") return;
680
681    // Only intercept the quote creation endpoint, not sub-routes.
682    if (options.url.indexOf("/homelocal/v1/quotes") === -1) return;
683
684    // Exclude sub-routes like /quotes/{id}/addons, /quotes/{id}/reservation.
685    var quotesIdx = options.url.indexOf("/quotes");
686    var afterQuotes = options.url.substring(quotesIdx + "/quotes".length);
687    if (afterQuotes && afterQuotes !== "" && afterQuotes.charAt(0) !== "?") {
688      return;
689    }
690
691    // Find the property_id in the data to look up the right state.
692    var propertyId = null;
693
694    if (typeof options.data === "string") {
695      var match = options.data.match(/property_id=([^&]+)/);
696      if (match) propertyId = match[1];
697    } else if (options.data && typeof options.data === "object") {
698      propertyId = options.data.property_id;
699    }
700
701    // Use property-specific state if available, otherwise fall back to URL params
702    // (covers explorer quote requests where no per-property widget exists).
703    var guestState = stateMap[propertyId] || null;
704
705    if (!guestState) {
706      var urlAdults = getUrlParam("adults");
707      var urlChildren = getUrlParam("children");
708      var urlPets = getUrlParam("pets");
709
710      if (!urlAdults && !urlPets) return;
711
712      guestState = {
713        adults: Math.max(1, parseInt(urlAdults, 10) || 1),
714        children: Math.max(0, parseInt(urlChildren, 10) || 0),
715        pets: 0,
716      };
717
718      if (urlPets === "yes") {
719        guestState.pets = 1;
720      } else if (urlPets && urlPets !== "no") {
721        guestState.pets = Math.max(0, parseInt(urlPets, 10) || 0);
722      }
723    }
724
725    if (typeof options.data === "string") {
726      options.data +=
727        "&adults=" + guestState.adults +
728        "&children=" + guestState.children +
729        "&pets=" + guestState.pets;
730    } else if (options.data && typeof options.data === "object") {
731      options.data.adults = guestState.adults;
732      options.data.children = guestState.children;
733      options.data.pets = guestState.pets;
734    }
735  });
736
737  // ── Persist guest state to sessionStorage for checkout page ─────
738  //
739  // The booking widget redirects to checkout via two paths (Central
740  // checkout-url AJAX or direct URL build) — neither carries guest
741  // params. We store the state before navigation and read it on the
742  // checkout page.
743
744  function persistGuestState() {
745    var guestState = null;
746    for (var pid in stateMap) {
747      if (stateMap.hasOwnProperty(pid)) {
748        guestState = stateMap[pid];
749        break;
750      }
751    }
752
753    if (!guestState) {
754      var urlAdults = getUrlParam("adults");
755      var urlPets = getUrlParam("pets");
756      if (!urlAdults && !urlPets) return;
757
758      guestState = {
759        adults: Math.max(1, parseInt(urlAdults, 10) || 1),
760        children: Math.max(0, parseInt(getUrlParam("children"), 10) || 0),
761        pets: 0,
762      };
763      if (urlPets === "yes") {
764        guestState.pets = 1;
765      } else if (urlPets && urlPets !== "no") {
766        guestState.pets = Math.max(0, parseInt(urlPets, 10) || 0);
767      }
768    }
769
770    if (guestState) {
771      try {
772        sessionStorage.setItem("hrtk_guest_state", JSON.stringify(guestState));
773      } catch (e) {}
774    }
775  }
776
777  // Save state whenever it changes (quote requests, apply button).
778  $(document).ajaxSend(function (event, jqXHR, settings) {
779    if (settings.url && settings.url.indexOf("/checkout-url") !== -1) {
780      persistGuestState();
781    }
782  });
783
784  // Also persist on any checkout button click (covers direct URL path).
785  $(document).on("click", ".checkout-button", function () {
786    persistGuestState();
787  });
788
789  // ── Explorer: replace pets Yes/No radio with numeric stepper ──────
790
791  function enhanceExplorerPets() {
792    var $explorer = $(".hfse");
793    if (!$explorer.length) return;
794
795    // Account-wide max pets fallback from toolkit settings (0 = no limit).
796    var maxPetsAccount = parseInt(i18n.maxPetsAccountWide, 10) || 0;
797
798    function maxPetsMessage() {
799      if (maxPetsAccount === 1) {
800        return i18n.maxPetsExplorerSingular || "Maximum 1 pet per booking";
801      }
802      return (i18n.maxPetsExplorerPlural || "Maximum %d pets per booking").replace("%d", maxPetsAccount);
803    }
804
805    // Convert the pets radio -> stepper inside ONE explorer container. Returns
806    // true when handled (converted now, or already converted on a prior pass);
807    // false when the guests form / pets radio is not yet in the DOM so the
808    // caller can retry.
809    function convertExplorerPets($modal) {
810      // Idempotent guard: already enhanced -> nothing to do.
811      if ($modal.find(".hrtk-gp-explorer-pets-wrap").length) return true;
812
813      // The guests modal can be portaled outside .hfse (UIkit moves modals to
814      // <body> on open), so resolve the owning explorer by climbing first, then
815      // falling back to the single explorer on the page.
816      var $container = $modal.closest(".hfse");
817      if (!$container.length) $container = $(".hfse").first();
818
819      var $petsRadioWrap = $modal.find('input[name="pets"][type="radio"]').closest(".hk-flex.hk-flex-middle");
820      if (!$petsRadioWrap.length) return false;
821
822      // Read current radio value.
823      var currentVal = $modal.find('input[name="pets"]:checked').val();
824      var petsVal = currentVal === "yes" ? 1 : 0;
825
826      // Also check URL for numeric pets value.
827      var urlPets = getUrlParam("pets");
828      if (urlPets && urlPets !== "no" && urlPets !== "yes") {
829        petsVal = Math.max(0, parseInt(urlPets, 10) || 0);
830      } else if (urlPets === "yes") {
831        petsVal = 1;
832      }
833
834      // Clamp initial value against the account-wide cap.
835      if (maxPetsAccount > 0 && petsVal > maxPetsAccount) {
836        petsVal = maxPetsAccount;
837      }
838
839      // Build the replacement stepper HTML (matches plugin's hfse-number-with-control pattern).
840      var maxAttr = maxPetsAccount > 0 ? ' max="' + maxPetsAccount + '"' : '';
841      var noticeHtml = maxPetsAccount > 0
842        ? '<div class="hrtk-gp-explorer-pets-notice hk-text-meta" style="display:none;margin-top:.25em;color:#b32d2e;"></div>'
843        : '';
844      var $replacement = $(
845        '<div class="hk-flex hk-flex-middle hk-margin hrtk-gp-explorer-pets-wrap">' +
846          '<div class="hk-width-3-5">' +
847            '<label class="hk-form-label">' + (i18n.pets || "Pets") + '</label>' +
848            '<div class="hk-text-meta">' + (i18n.bringingPet || "Bringing a pet?") + '</div>' +
849          '</div>' +
850          '<div class="hk-width-2-5">' +
851            '<div class="hk-form-controls hfse-number-with-control">' +
852              '<div class="reduce-btn" title="-">-</div>' +
853              '<input type="text" min="0" step="1"' + maxAttr + ' name="pets" class="number-input" value="' + petsVal + '" autocomplete="off" />' +
854              '<div class="increase-btn" title="+">+</div>' +
855            '</div>' +
856            noticeHtml +
857          '</div>' +
858        '</div>'
859      );
860
861      // Replace radio row with stepper row.
862      $petsRadioWrap.replaceWith($replacement);
863
864      // The plugin's explorer.js `numberControlsInit` already binds delegated
865      // click handlers on `.hfse-number-with-control .reduce-btn/.increase-btn`
866      // inside the guests modal — it does the +/- increment and triggers change.
867      // We deliberately don't bind our own click handlers here; doing so caused
868      // a +2 / -2 double-increment because both delegates fired per click.
869      var $petsInput = $replacement.find('input[name="pets"]');
870      var $petsNotice = $replacement.find(".hrtk-gp-explorer-pets-notice");
871      var noticeTimer;
872
873      function showMaxNotice() {
874        if (!$petsNotice.length) return;
875        $petsNotice.text(maxPetsMessage()).stop(true, true).fadeIn(150);
876        clearTimeout(noticeTimer);
877        noticeTimer = setTimeout(function () {
878          $petsNotice.fadeOut(150);
879        }, 3000);
880      }
881
882      // Enforce the cap when the user tries to increment past it. Direct
883      // (non-delegated) click handlers fire BEFORE the plugin's delegated
884      // `.increase-btn` handler bound on the ancestor modal, so calling
885      // stopImmediatePropagation here cancels that one click — without
886      // re-introducing the v3.22.0 double-increment bug, because we only
887      // short-circuit when already at the cap (otherwise we yield to the
888      // plugin's handler, which performs the single increment).
889      if (maxPetsAccount > 0) {
890        $replacement.find(".increase-btn").on("click", function (e) {
891          var current = parseInt($petsInput.val(), 10) || 0;
892          if (current >= maxPetsAccount) {
893            e.stopImmediatePropagation();
894            e.preventDefault();
895            showMaxNotice();
896            return false;
897          }
898          // Otherwise: do nothing and let the plugin's delegated handler increment.
899        });
900
901        // Defensive clamp on any other input mutation (URL hydration, reset, etc.).
902        $petsInput.on("change", function () {
903          var current = parseInt($petsInput.val(), 10) || 0;
904          if (current > maxPetsAccount) {
905            $petsInput.val(maxPetsAccount);
906            showMaxNotice();
907          }
908        });
909      }
910
911      // Patch the explorer's updateGuestsInput to read numeric pets.
912      // The explorer builds a display value like "3" or "3, Pets".
913      // We override the display to show "3, 2 Pets" instead.
914      var $guestsValueInput = $container.find(".hfse-form-guests-value");
915      if ($guestsValueInput.length) {
916        $petsInput.on("change", function () {
917          var adults = parseInt($modal.find('input[name="adults"]').val(), 10) || 1;
918          var children = parseInt($modal.find('input[name="children"]').val(), 10) || 0;
919          var pets = parseInt($petsInput.val(), 10) || 0;
920          var total = adults + children;
921          var display = total + "";
922          if (pets > 0) {
923            display += ", " + pets + " " + (pets === 1 ? (i18n.pet || "Pet") : (i18n.petsPlural || "Pets"));
924          }
925          $guestsValueInput.val(display);
926        });
927
928        // Also patch adults/children change to include pets in display.
929        $modal.find('input[name="adults"], input[name="children"]').on("change", function () {
930          $petsInput.trigger("change");
931        });
932
933        // Set initial display.
934        if (petsVal > 0) {
935          $petsInput.trigger("change");
936        }
937      }
938
939      // Clear pets on "Start Over". Two bugs to handle:
940      //  1. The plugin fires reset on $container (the .hfse root), not on the
941      //     descendant $modal — and reset does not bubble down — so the old
942      //     $modal.on("reset") handler never fired.
943      //  2. The numeric stepper value never equals default_params.pets ("no"),
944      //     so the plugin's updateUrl re-adds pets=0 to the URL even after a
945      //     reset. Strip pets/pet_count from the URL after the plugin's
946      //     post-reset performFilter()/updateUrl() (a ~5ms timer) runs.
947      $container.on("reset", function () {
948        $petsInput.val("0").trigger("change");
949        setTimeout(function () {
950          var u = new URL(window.location.href);
951          u.searchParams.delete("pets");
952          u.searchParams.delete("pet_count");
953          window.history.replaceState({}, "", u);
954        }, 80);
955      });
956
957      return true;
958    }
959
960    // The explorer renders its guests modal asynchronously and may portal it to
961    // <body> when opened, so a single document-ready pass misses it. Convert
962    // every guests modal currently in the DOM (found GLOBALLY, not scoped to
963    // .hfse), retry briefly on an interval, AND keep a persistent debounced
964    // MutationObserver on <body> so a modal opened or rebuilt at any time still
965    // gets its pets radio converted. convertExplorerPets is idempotent, so
966    // re-running on every mutation is cheap.
967    function runAllExplorerPets() {
968      var $modals = $(".hfse-form-guests-options");
969      if (!$modals.length) return false; // guests form not rendered yet
970      var allDone = true;
971      $modals.each(function () {
972        if (!convertExplorerPets($(this))) allDone = false;
973      });
974      return allDone;
975    }
976
977    runAllExplorerPets();
978
979    var petsTries = 0;
980    var PETS_MAX_TRIES = 50; // ~10s of active polling at 200ms
981    var petsInterval = setInterval(function () {
982      if (runAllExplorerPets() || ++petsTries >= PETS_MAX_TRIES) {
983        clearInterval(petsInterval);
984        petsInterval = null;
985      }
986    }, 200);
987
988    // Persistent observer: never torn down (the modal can open long after the
989    // poll window closes, and may be rebuilt on each open). Debounced; the
990    // conversion itself is guarded/idempotent.
991    if (window.MutationObserver) {
992      var petsDebounce = null;
993      var petsObserver = new MutationObserver(function () {
994        if (petsDebounce) return;
995        petsDebounce = setTimeout(function () {
996          petsDebounce = null;
997          runAllExplorerPets();
998        }, 50);
999      });
1000      petsObserver.observe(document.body, { childList: true, subtree: true });
1001    }
1002  }
1003
1004  // ── Explorer: append search params to property card links ───────
1005
1006  function enhanceExplorerLinks() {
1007    var $explorer = $(".hfse");
1008    if (!$explorer.length) return;
1009
1010    function updateCardLinks() {
1011      var params = new URLSearchParams(window.location.search);
1012      var checkin = params.get("checkin");
1013      var checkout = params.get("checkout");
1014      var adults = params.get("adults");
1015      var children = params.get("children");
1016      var pets = params.get("pets");
1017
1018      // Only proceed if there are guest/date params to append.
1019      if (!checkin && !adults && !pets) return;
1020
1021      // Default adults to 1 when dates are present (widget needs at least 1 for quote).
1022      if (checkin && !adults) adults = "1";
1023
1024      $explorer.find(".hfse-property-card").each(function () {
1025        var $link = $(this);
1026        var baseUrl = $link.attr("href");
1027        if (!baseUrl) return;
1028
1029        var url;
1030        try {
1031          url = new URL(baseUrl, window.location.origin);
1032        } catch (e) {
1033          return;
1034        }
1035
1036        // Clear any previous booking params we may have set.
1037        ["checkin", "checkout", "adults", "children", "pets", "guests"].forEach(
1038          function (p) {
1039            url.searchParams.delete(p);
1040          }
1041        );
1042
1043        if (checkin) url.searchParams.set("checkin", checkin);
1044        if (checkout) url.searchParams.set("checkout", checkout);
1045        if (adults) url.searchParams.set("adults", adults);
1046        if (children && children !== "0")
1047          url.searchParams.set("children", children);
1048        if (pets && pets !== "no" && pets !== "0") url.searchParams.set("pets", pets);
1049
1050        // Always set combined guests for backward compat with booking widget PHP.
1051        // This ensures the PHP-rendered #guests dropdown matches the stepper state,
1052        // even when adults=1 (prevents base_guests override on property page).
1053        var totalGuests =
1054          (parseInt(adults, 10) || 1) + (parseInt(children, 10) || 0);
1055        url.searchParams.set("guests", totalGuests);
1056
1057        $link.attr("href", url.toString());
1058      });
1059    }
1060
1061    // Debounced MutationObserver on the properties container.
1062    var $results = $explorer.find(".hfse-properties-container");
1063    if ($results.length) {
1064      var debounceTimer = null;
1065      var observer = new MutationObserver(function () {
1066        clearTimeout(debounceTimer);
1067        debounceTimer = setTimeout(updateCardLinks, 50);
1068      });
1069      observer.observe($results[0], { childList: true, subtree: true });
1070    }
1071
1072    // Re-run on back/forward browser navigation.
1073    $(window).on("popstate", updateCardLinks);
1074
1075    // Initial run.
1076    updateCardLinks();
1077  }
1078
1079  // ── Checkout summary: replace static guest count with breakdown ─
1080
1081  function enhanceCheckoutSummary() {
1082    var $checkout = $(".homelocal-checkout-overview");
1083    if (!$checkout.length) return;
1084
1085    // Read guest state from sessionStorage (set on property page before redirect).
1086    var guestState = null;
1087    try {
1088      var stored = sessionStorage.getItem("hrtk_guest_state");
1089      if (stored) guestState = JSON.parse(stored);
1090    } catch (e) {}
1091
1092    // Fall back to URL params.
1093    if (!guestState) {
1094      var adults = parseInt(getUrlParam("adults"), 10) || 0;
1095      var children = parseInt(getUrlParam("children"), 10) || 0;
1096      var pets = getUrlParam("pets");
1097      var petCount = 0;
1098
1099      if (pets === "yes") {
1100        petCount = 1;
1101      } else if (pets && pets !== "no") {
1102        petCount = parseInt(pets, 10) || 0;
1103      }
1104
1105      if (!adults && !petCount) return;
1106
1107      guestState = {
1108        adults: Math.max(1, adults),
1109        children: children,
1110        pets: petCount,
1111      };
1112    }
1113
1114    var summary = formatSummary(guestState);
1115
1116    // Find the static "Guests" label in the checkout dates grid and replace its value.
1117    // Match against both the English string and the localized toolkit str
1117ing so this
1118    // works on translated sites where the plugin's homelocal domain is translated too.
1119    var guestsLabels = ["Guests", i18n.guests || "Guests"];
1120    $checkout.find(".hk-grid .hk-width-1-2").each(function () {
1121      var $col = $(this);
1122      var labelText = $col.find("label").text().trim();
1123      if (guestsLabels.indexOf(labelText) !== -1) {
1124        $col.find(".hk-form-controls").text(summary);
1125      }
1126    });
1127  }
1128
1129  // ── Bootstrap: init guest pickers after widgets are ready ──────
1130
1131  $(function () {
1132    // Enhance checkout summary with guest breakdown.
1133    enhanceCheckoutSummary();
1134
1135    // Replace explorer pets radio with numeric stepper.
1136    enhanceExplorerPets();
1137
1138    // Enhance explorer property card links with search params.
1139    enhanceExplorerLinks();
1140    var attempts = 0;
1141    var maxAttempts = 50;
1142
1143    var checkInterval = setInterval(function () {
1144      attempts++;
1145
1146      var shortcodes = window.homerunner_booking_widget_shortcodes;
1147      if (!shortcodes || !shortcodes.length) {
1148        if (attempts >= maxAttempts) {
1149          clearInterval(checkInterval);
1150          markReady();
1151        }
1152        return;
1153      }
1154
1155      // Check if at least one widget has been initialized (has the data element).
1156      var hasData = false;
1157      for (var i = 0; i < shortcodes.length; i++) {
1158        if ($(shortcodes[i].selector).find(".hrtk-gp-data").length) {
1159          hasData = true;
1160          break;
1161        }
1162      }
1163
1164      if (!hasData) {
1165        if (attempts >= maxAttempts) {
1166          clearInterval(checkInterval);
1167          markReady();
1168        }
1169        return;
1170      }
1171
1172      clearInterval(checkInterval);
1173
1174      try {
1175        shortcodes.forEach(function (shortcode) {
1176          initGuestPicker(shortcode);
1177        });
1178      } finally {
1179        markReady();
1180      }
1181    }, 100);
1182  });
1183})(jQuery);

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.