PageSourceSearch

https://mountainmamacabins.com/wp-content/plugins/homerunner/assets/js/booking-widget.js?ver=2.6.6

js mountainmamacabins.com collected 2026-09-30 23:27:41 UTC 62,903 bytes, 2,082 lines download raw bytes

1(function ($, settings) {
2  $.fn.homelocalBookingWidget = function (options) {
3    const app = this;
4    const $widget = $(this);
5    options = $.extend({}, options);
6    options.max_num_days = parseInt(options.max_num_days, 10) || 365;
7    options.max_stay = parseInt(options.max_stay, 10) || 365;
8
9    let selector = {
10      widget: ".homelocal-booking-widget",
11      widgetBody: ".homelocal-booking-widget-body",
12      widgetLoader: ".widget-loader",
13      priceLoader: ".price-loader",
14      bookingOverview: ".homelocal-booking-overview",
15      quoteNotice: ".quote-notice",
16      couponNotice: ".coupon-notice",
17      checkoutBtn: ".checkout-button",
18      rentUnit: ".rent-unit",
19      totalNights: ".total-nights",
20      priceDetailsModal: "#homelocal-booking-price-details-modal",
21      stickyWidgetModal: "#homelocal-sticky-booking-widget-modal",
22      stickyWidgetContainer: ".homelocal-sticky-booking-widget-container",
23      stickyBtn: ".modal-button",
24      stickyRentUnit: ".rent-unit",
25      stickyTotalNights: ".total-nights",
26      stickyPrice: ".price",
27      stickyNotice: ".notice",
28      stickyPriceLoader: ".price-loader",
29      stickyDates: ".dates",
30      calendarModal: "#homelocal-availability-calendar-modal",
31    };
32
33    const $sticky = $(selector.stickyWidgetContainer);
34
35    const calendarUtils = new HomeLocalCalendarUtils(
36      homerunner_calendar_utils_settings,
37      {
38        min_stay: options.min_stay,
39        max_stay: options.max_stay,
40        max_num_days: options.max_num_days,
41        timezone: options.timezone,
42      },
43    );
44
45    let quote = {};
46    // Coupon event waiting for the next quote render: "applied" | "removed" | null (#877).
47    let pendingCouponEvent = null;
48    let addons = [];
49    const travelCoverageFeeTypes = Array.isArray(
50      settings.travel_coverage_fee_types,
51    )
52      ? settings.travel_coverage_fee_types
53      : ["travel_insurance"];
54    const travelInsuranceGroup = `travel_insurance_${Math.random()
55      .toString(36)
56      .slice(2, 9)}`;
57    let ratePlanSelected = options.rateplan_id || null;
58    const dateFormat = "yy-mm-dd";
59    const luxonDateFormat = "yyyy-MM-dd";
60
61    app.loadPrice = (done) => {
62      $widget.addClass("hfse-loading");
63
64      $.ajax({
65        url: `${settings.wp_api}wp/v2/properties/${options.property_id}/calendar`,
66        type: "GET",
67        data: {
68          _: new Date().getTime(),
69        },
70        beforeSend: function (xhr) {
71          if (settings.user_nonce) {
72            xhr.setRequestHeader("X-Homerunner-Nonce", settings.user_nonce);
73          }
74        },
75      })
76        .done((data) => {
77          if (typeof done === "function") {
78            done(data);
79          }
80        })
81        .always(() => {
82          $widget.removeClass("hfse-loading");
83        });
84    };
85
86    app.init = () => {
87      if ($(selector.stickyWidgetContainer).length > 0) {
88        $(selector.stickyWidgetContainer).appendTo(".hk-scope-modal");
89      }
90
91      if (options.show_min_stay) {
92        app.renderMinimumNights();
93      }
94
95      app.registerActions();
96      if (options.show_coupon) {
97        app.registerCouponActions();
98      }
99      app.registerAddonActions();
100
101      if (options.show_datepicker) {
102        $widget.find(selector.widgetLoader).show();
103        if (settings.use_calendar) {
104          app.initCalendar();
105        } else {
106          app.initDatepicker();
107        }
108      } else if (options.quote_id) {
109        app.refreshQuote(options.quote_id);
110      } else if (options.checkin && options.checkout && options.guests) {
111        app.updateBookingCost();
112      }
113    };
114
115    app.debounce = (func, timeout = 300) => {
116      let timer;
117      return (...args) => {
118        clearTimeout(timer);
119        timer = setTimeout(() => {
120          func.apply(this, args);
121        }, timeout);
122      };
123    };
124
125    app.onAvailabilityCalendarLoaded = (e, property_id, data) => {
126      if (
127        property_id === options.property_id &&
128        !calendarUtils.hasPriceData()
129      ) {
130        calendarUtils.setPriceData(data);
131
132        if ("first" === settings.min_stay_source && options.show_min_stay) {
133          app.renderMinimumNights();
134        }
135      }
136    };
137
138    app.updateUrl = () => {
139      const url = new URL(window.location.href);
140      const checkin = $widget.find("#checkin").val();
141      const checkout = $widget.find("#checkout").val();
142      const guests = $widget.find("#guests").val();
143
144      if (checkin) {
145        url.searchParams.set("checkin", checkin);
146      } else {
147        url.searchParams.delete("checkin");
148      }
149
150      if (checkout) {
151        url.searchParams.set("checkout", checkout);
152      } else {
153        url.searchParams.delete("checkout");
154      }
155
156      if (guests && guests !== "1") {
157        url.searchParams.set("guests", guests);
158      } else {
159        url.searchParams.delete("guests");
160      }
161
162      window.history.replaceState({}, "", url);
163    };
164
165    app.initCalendar = () => {
166      $(document).on(
167        "homelocal_availability_calendar_loaded",
168        app.onAvailabilityCalendarLoaded,
169      );
170
171      $widget.find("#checkin_placeholder").on("click", () => {
172        hkUIkit.modal(selector.calendarModal).show();
173      });
174
175      $widget.find("#checkout_placeholder").on("click ", () => {
176        hkUIkit.modal(selector.calendarModal).show();
177      });
178
179      $widget.on("change", "#checkout", () => {
180        hkUIkit.modal(selector.calendarModal).hide();
181
182        app.updateChekinPlaceholder();
183        app.updateChekoutPlaceholder();
184        app.updateUrl();
185      });
186
187      $widget.on(
188        "change",
189        "#checkout,#guests",
190        app.debounce(() => {
191          app.updateBookingCost();
192        }, 10),
193      );
194
195      $widget.on("change", "#guests", () => {
196        app.updateUrl();
197      });
198
199      const checkin = $widget.find("#checkin").val();
200      const checkout = $widget.find("#checkout").val();
201
202      if (checkin) {
203        app.updateChekinPlaceholder();
204      }
205
206      if (checkout) {
207        app.updateChekoutPlaceholder();
208      }
209
210      if (checkin && checkout) {
211        if (calendarUtils.hasPriceData()) {
212          app.updateBookingCost();
213        } else {
214          $(document).on(
215            "homelocal_availability_calendar_loaded",
216            app.debounce(app.updateBookingCost, 300),
217          );
218        }
219      } else {
220        $widget.find(selector.widgetBody).show();
221        $widget.find(selector.widgetLoader).hide();
222
223        $widget
224          .find(selector.checkoutBtn)
225          .prop("disabled", false)
226          .text(settings.select_dates)
227          .show();
228        if (options.sticky) {
229          $sticky
230            .find(selector.stickyBtn)
231            .prop("disabled", false)
232            .text(settings.select_dates);
233        }
234
235        app.showNoDatesRent();
236      }
237    };
238
239    app.initDatepicker = () => {
240      $widget
241        .find("#checkin_placeholder,#checkout_placeholder")
242        .prop("disabled", true);
243
244      app.loadPrice((data) => {
245        $widget
246          .find("#checkin_placeholder,#checkout_placeholder")
247          .prop("disabled", false);
248
249        calendarUtils.setPriceData(data);
250
251        $(document).trigger("homelocal_calendar_data_loaded", [
252          options.property_id,
253          data,
254        ]);
255
256        const minDate = calendarUtils.getFirstCheckinDay()
257          ? calendarUtils.getFirstCheckinDay()
258          : 1;
259
260        const checkinDp = $widget.find("#checkin").datepicker({
261          minDate: calendarUtils.getDateZero(minDate).toFormat(luxonDateFormat),
262          maxDate: options.max_num_days,
263          dateFormat: dateFormat,
264          beforeShowDay: (d) => {
265            const day = $.datepicker.formatDate(dateFormat, d);
266            if (!calendarUtils.canCheckin(day).status) {
267              return [false, "unavailable", settings.date_unavailable];
268            } else {
269              return [true, "available", "available"];
270            }
271          },
272        });
273
274        const checkoutDp = $widget.find("#checkout").datepicker({
275          minDate: 1,
276          maxDate: 1,
277          dateFormat: dateFormat,
278          beforeShowDay: (d) => {
279            const checkin = checkinDp.val();
280            if (!checkin) {
281              return [false, "unavailable", settings.date_unavailable];
282            }
283
284            const checkout = $.datepicker.formatDate(dateFormat, d);
285
286            if (!calendarUtils.canCheckinout(checkin, checkout).status) {
287              return [false, "unavailable", settings.date_unavailable];
288            } else {
289              return [true, "available", "available"];
290            }
291          },
292        });
293
294        // show checkout selector after changing checkin
295        checkinDp.on("change", function () {
296          const checkin = app.getFormattedDate(this);
297
298          app.updateChekinPlaceholder();
299
300          if (null !== checkin) {
301            const checkinDate = calendarUtils.getDateZero(checkin);
302            const checkoutMin = checkinDate.plus({
303              days: calendarUtils.getMinstay(checkin),
304            });
305
306            app.renderMinimumNights(calendarUtils.getMinstay(checkin));
307
308            checkoutDp.datepicker(
309              "option",
310              "minDate",
311              checkoutMin.toFormat(luxonDateFormat),
312            );
313
314            const checkoutMax = checkinDate.plus({ days: options.max_stay });
315
316            checkoutDp.datepicker(
317              "option",
318              "maxDate",
319              checkoutMax.toFormat(luxonDateFormat),
320            );
321
322            setTimeout(function () {
323              checkoutDp.datepicker("show");
324            });
325          }
326        });
327
328        checkoutDp.on("change", function () {
329          app.updateChekinPlaceholder();
330          app.updateChekoutPlaceholder();
331        });
332
333        $widget.find("#checkin_placeholder").on("focus", function () {
334          $widget.find("#checkin").datepicker("show");
335        });
336
337        $widget.find("#checkout_placeholder").on("focus", function () {
338          $widget.find("#checkout").datepicker("show");
339        });
340
341        if ("first" === settings.min_stay_source && options.show_min_stay) {
342          app.renderMinimumNights();
343        }
344      });
345
346      $widget.on(
347        "change",
348        "#checkout,#guests",
349        app.debounce(app.updateBookingCost),
350      );
351
352      const checkin = $widget.find("#checkin").val();
353      const checkout = $widget.find("#checkout").val();
354
355      if (checkin) {
356        app.updateChekinPlaceholder();
357      }
358
359      if (checkout) {
360        app.updateChekoutPlaceholder();
361      }
362
363      if (checkin && checkout) {
364        if (calendarUtils.hasPriceData()) {
365          app.updateBookingCost();
366        } else {
367          $(document).on(
368            "homelocal_calendar_data_loaded",
369            app.updateBookingCost,
370          );
371        }
372      } else {
373        $widget.find(selector.widgetBody).show();
374        $widget.find(selector.widgetLoader).hide();
375
376        $widget
377          .find(selector.checkoutBtn)
378          .prop("disabled", false)
379          .text(settings.select_dates)
380          .show();
381        if (options.sticky) {
382          $sticky
383            .find(selector.stickyBtn)
384            .prop("disabled", false)
385            .text(settings.select_dates);
386        }
387
388        app.showNoDatesRent();
389      }
390    };
391
392    app.updateChekinPlaceholder = () => {
393      const checkin = $widget.find("#checkin").val();
394
395      if (checkin) {
396        $widget
397          .find("#checkin_placeholder")
398          .val(
399            calendarUtils.getDateZero(checkin).toFormat(options.date_format),
400          );
401      } else {
402        $widget.find("#checkin_placeholder").val("");
403      }
404    };
405
406    app.updateChekoutPlaceholder = () => {
407      const checkout = $widget.find("#checkout").val();
408
409      if (checkout) {
410        $widget
411          .find("#checkout_placeholder")
412          .val(
413            calendarUtils.getDateZero(checkout).toFormat(options.date_format),
414          );
415      } else {
416        $widget.find("#checkout_placeholder").val("");
417      }
418    };
419
420    app.showNoDatesRent = () => {
421      if (options.show_rent) {
422        if (settings.price_without_dates === "base_price") {
423          app.renderBaseRent(options.base_rent);
424        } else if (settings.price_without_dates === "average_price") {
425          app.renderAvgRent(options.avg_rent);
426        } else if (settings.price_without_dates === "lowest_price") {
427          app.renderLowestRent();
428        } else {
429          app.hideRent();
430        }
431      }
432    };
433
434    app.renderRentAfterDate = (total, totalNights, discount = 0) => {
435      let price = 0,
436        actualPrice = parseFloat(total) / totalNights;
437      if (discount > 0) {
438        price = parseFloat(total - discount) / totalNights;
439      } else {
440        price = parseFloat(total) / totalNights;
441      }
442
443      let rentText = settings.rent_after_date_template.replace(
444        "{price}",
445        `<span class="rent-after-date rent-amount">${app.price(price)}</span>`,
446      );
447
448      if (actualPrice && actualPrice > price) {
449        rentText = settings.rent_after_date_template.replace(
450          "{price}",
451          `<del class="rent-deleted">${app.price(
452            actualPrice,
453          )}</del><span class="rent-after-date rent-amount">${app.price(
454            price,
455          )}</span>`,
456        );
457      }
458
459      $sticky.find(selector.stickyRentUnit).html(rentText).show();
460
461      if (options.show_bedroom_price && options.bedrooms_count > 1) {
462        rentText += `<span class="rent-bedroom-sep">&middot;</span><span class="bedroom-price-text">${settings.per_bedroom_text.replace(
463          "{price}",
464          app.price(Math.round(price / options.bedrooms_count)),
465        )}</span>`;
466      }
467
468      $widget.find(selector.rentUnit).html(rentText).show();
469    };
470
471    app.renderBaseRent = (price) => {
472      $widget
473        .find(selector.rentUnit)
474        .html(
475          settings.base_rent_template.replace(
476            "{price}",
477            `<span class="base-rent rent-amount">${app.price(price)}</span>`,
478          ),
479        )
480        .show();
481
482      $sticky
483        .find(selector.stickyRentUnit)
484        .html(
485          settings.base_rent_template.replace(
486            "{price}",
487            `<span class="base-rent rent-amount">${app.price(price)}</span>`,
488          ),
489        )
490        .show();
491    };
492
493    app.renderAvgRent = (price) => {
494      $widget
495        .find(selector.rentUnit)
496        .html(
497          settings.avg_price_template.replace(
498            "{price}",
499            `<span class="average-rent rent-amount">${app.price(price)}</span>`,
500          ),
501        )
502        .show();
503
504      $sticky
505        .find(selector.stickyRentUnit)
506        .html(
507          settings.avg_price_template.replace(
508            "{price}",
509            `<span class="average-rent rent-amount">${app.price(price)}</span>`,
510          ),
511        )
512        .show();
513    };
514
515    app.renderLowestRent = () => {
516      const price = options.lowest_rent;
517      $widget
518        .find(selector.rentUnit)
519        .html(
520          settings.lowest_rent_template.replace(
521            "{price}",
522            `<span class="lowest-rent rent-amount">${app.price(price)}</span>`,
523          ),
524        )
525        .show();
526
527      $sticky
528        .find(selector.stickyRentUnit)
529        .html(
530          settings.lowest_rent_template.replace(
531            "{price}",
532            `<span class="lowest-rent rent-amount">${app.price(price)}</span>`,
533          ),
534        )
535        .show();
536    };
537
538    app.hideRent = () => {
539      $widget.find(selector.rentUnit).empty().hide();
540
541      $sticky.find(selector.stickyRentUnit).hide();
542    };
543
544    app.renderMinimumNights = (minStay = null) => {
545      if (!minStay) {
546        minStay = options.min_stay;
547
548        if ("first" === settings.min_stay_source) {
549          if (calendarUtils.hasPriceData()) {
550            const day = calendarUtils.getFirstCheckinDay();
551            if (day && calendarUtils.canCheckin(day).status) {
552              minStay = calendarUtils.getMinstay(day);
553            }
554          }
555        }
556      }
557
558      let template = settings.min_night_text;
559      if (parseInt(minStay, 10) > 1) {
560        template = settings.min_nights_text;
561      }
562
563      $widget
564        .find(selector.totalNights)
565        .text(template.replace("{nights}", minStay))
566        .show();
567
568      $sticky
569        .find(selector.stickyTotalNights)
570        .text(template.replace("{nights}", minStay))
571        .show();
572    };
573
574    app.hideMinimumNights = () => {
575      $widget.find(selector.totalNights).hide();
576
577      $sticky.find(selector.stickyTotalNights).hide();
578    };
579
580    app.renderTotal = (total, totalBeforeTax, discountItems = []) => {
581      if (options.total_after_tax) {
582        $widget.find(".total-price-label").text(settings.total_after_tax_label);
583        $widget.find(".total-price").text(app.price(total));
584      } else {
585        $widget
586          .find(".total-price-label")
587          .text(settings.total_before_tax_label);
588
589        $widget.find(".total-price").text(app.price(totalBeforeTax));
590      }
591
592      // const $discountItems = $widget.find(".discount-items");
593      // $discountItems.empty();
594      // discountItems.forEach((item) => {
595      //   const lineItem = $(`
596      // 		<div class="hk-flex hk-flex-between">
597      // 			<span class="hk-text-bold">${item.title}</span>
598      // 			<span class="hk-text-bold">-${app.price(item.amount)}</span>
599      // 		</div>
600      // 	`);
601      //   $discountItems.append(lineItem);
602      // });
603      // $discountItems.show();
604    };
605
606    app.renderNights = (night) => {
607      if (night === 1) {
608        $widget
609          .find(selector.totalNights)
610          .text(settings.total_night_text.replace("{nights}", night))
611          .show();
612      } else {
613        $widget
614          .find(selector.totalNights)
615          .text(settings.total_nights_text.replace("{nights}", night))
616          .show();
617      }
618    };
619
620    app.registerActions = () => {
621      $widget.on("change", 'input[name="rateplan"]', function () {
622        if (!quote || !quote.id) {
623          return false;
624        }
625
626        ratePlanSelected = $(this).val();
627        const item = quote.rateplans.find(
628          (item) => item.id === ratePlanSelected,
629        );
630
631        if (item) {
632          app.updatePriceBreakdown();
633
634          // Percentage addons are priced against the selected rate plan.
635          if (quote.available_addons) {
636            app.renderAddons();
637          }
638
639          if (options.show_coupon) {
640            app.renderCoupon();
641          }
642        } else {
643          console.warn(settings.invalid_rateplan);
644        }
645      });
646
647      $widget.on("click", selector.checkoutBtn, function () {
648        const checkin = $widget.find("#checkin").val();
649        const checkout = $widget.find("#checkout").val();
650
651        if (!checkin) {
652          if (settings.use_calendar) {
653            $widget.find("#checkin_placeholder").trigger("click");
654          } else {
655            // $widget.find("#checkin").focus();
656            $widget.find("#checkin_placeholder").focus();
657          }
658          return false;
659        } else if (!checkout) {
660          if (settings.use_calendar) {
661            $widget.find("#checkout_placeholder").trigger("click");
662          } else {
663            $widget.find("#checkout_placeholder").focus();
664          }
665          return false;
666        }
667
668        if (!quote.id) {
669          return false;
670        }
671
672        if (options.reservation_target === "hosted") {
673          $widget.find(selector.bookingOverview).hide();
674          $widget.find(selector.priceLoader).show();
675          $widget
676            .find(selector.checkoutBtn)
677            .prop("disabled", true)
678            .text(options.booking_button_text_processing);
679
680          $.ajax({
681            url:
682              settings.plugin_api +
683              "quotes/" +
684              quote.id +
685              "/checkout-url?_=" +
686              new Date().getTime(),
687            type: "GET",
688            data: {
689              rateplan_id: ratePlanSelected,
690            },
691          })
692            .done(function (response) {
693              if (response.success) {
694                window.location.href = response.data.url;
695              } else {
696                app.quoteNotice(response.message, "warning");
697              }
698            })
699            .fail(function (jqXHR, textStatus, errorThrown) {
700              app.quoteNotice(jqXHR.responseJSON.message, "warning");
701            })
702            .always(function () {
703              $widget.find(selector.bookingOverview).show();
704              $widget.find(selector.priceLoader).hide();
705              $widget
706                .find(selector.checkoutBtn)
707                .prop("disabled", false)
708                .text(options.booking_button_text);
709            });
710
711          return false;
712        }
713
714        if ($("#homelocal-booking-widget-checkout-modal").length > 0) {
715          hkUIkit.modal("#homelocal-booking-widget-checkout-modal").show();
716          return false;
717        }
718
719        $widget.find(selector.checkoutBtn).prop("disabled", true);
720
721        let url = options.checkout_url;
722        // add qid to the url, check if ? already exists
723        if (url.indexOf("?") > -1) {
724          url += "&qid=" + quote.id.toString();
725        } else {
726          url += "?qid=" + quote.id.toString();
727        }
728
729        if (ratePlanSelected) {
730          url += "&rpid=" + ratePlanSelected;
731        }
732
733        window.location.href = url;
734      });
735
736      $(document).on(
737        "click",
738        ".homelocal-checkout-page-select-btn",
739        function () {
740          let url = $(
741            'input[name="homelocal_checkout_provider_url"]:checked',
742          ).val();
743
744          if (!url) {
745            return false;
746          }
747
748          if (url.indexOf("?") > -1) {
749            // add qid to the url, check if ? already exists
750            url += "&qid=" + quote.id.toString();
751          } else {
752            url += "?qid=" + quote.id.toString();
753          }
754
755          if (ratePlanSelected) {
756            url += "&rpid=" + ratePlanSelected;
757          }
758
759          window.location.href = url;
760          return false;
761        },
762      );
763
764      $(document).on(
765        "change",
766        'input[name="homelocal_checkout_provider_url"]',
767        function () {
768          // add a active class to closest label.
769          $(
770            "#homelocal-booking-widget-checkout-modal .homelocal-booking-widget-checkout-item",
771          ).removeClass("hk-selected");
772          $(this)
773            .closest(".homelocal-booking-widget-checkout-item")
774            .addClass("hk-selected");
775        },
776      );
777
778      $widget.on("updating", function () {
779        $widget
780          .find(selector.checkoutBtn)
781          .prop("disabled", true)
782          .text(options.booking_button_text_processing)
783          .show();
784
785        $sticky
786          .find(selector.stickyBtn)
787          .prop("disabled", true)
788          .text(options.booking_button_text_processing);
789
790        $sticky.find(selector.stickyPriceLoader).show();
791        $sticky.find(selector.stickyNotice).empty();
792        $sticky.find(selector.stickyPrice).hide();
793        $sticky.find(selector.stickyTotalNights).hide();
794      });
795
796      $widget.on("unavailable", function (e, error) {
797        $widget
798          .find(selector.checkoutBtn)
799          .prop("disabled", true)
800          .text(options.booking_button_text)
801          .show();
802
803        $sticky
804          .find(selector.stickyBtn)
805          .text(options.booking_button_text)
806          .prop("disabled", false);
807
808        $sticky
809          .find(selector.stickyNotice)
810          .html(`<div class="hk-text-danger">${error}</div>`)
811          .show();
812
813        $sticky.find(selector.stickyDates).show();
814        $sticky.find(selector.stickyRentUnit).hide();
815        $sticky.find(selector.stickyPrice).hide();
816        $sticky.find(selector.stickyPriceLoader).hide();
817      });
818
819      $widget.on("available", function () {
820        $widget
821          .find(selector.checkoutBtn)
822          .prop("disabled", false)
823          .text(options.booking_button_text)
824          .show();
825
826        $sticky
827          .find(selector.stickyBtn)
828          .prop("disabled", false)
829          .text(options.booking_button_text)
830          .show();
831
832        $sticky.find(selector.stickyPrice).show();
833        $sticky.find(selector.stickyNotice).empty();
834        $sticky.find(selector.stickyPriceLoader).hide();
835        $sticky.find(selector.stickyTotalNights).hide();
836      });
837
838      let priceFromModalWidget = false;
839      $(document).on(
840        "click",
841        "#homelocal-sticky-booking-widget-modal .price-details-modal-button",
842        function () {
843          priceFromModalWidget = true;
844        },
845      );
846
847      $(document).on("beforeshow", selector.stickyWidgetModal, function () {
848        $widget.appendTo($(`${selector.stickyWidgetModal} .hk-widget-content`));
849        $sticky.hide();
850      });
851
852      $(document).on("hidden", selector.stickyWidgetModal, function () {
853        $widget.appendTo($(".homelocal-booking-widget-container"));
854        $sticky.show();
855      });
856
857      if (options.price_modal) {
858        $(document).on("beforeshow", selector.priceDetailsModal, function (e) {
859          $sticky.hide();
860        });
861
862        $(document).on("hidden", selector.priceDetailsModal, function () {
863          if (priceFromModalWidget) {
864            priceFromModalWidget = false;
865            hkUIkit.modal(selector.stickyWidgetModal).show();
866          } else {
867            $sticky.show();
868          }
869        });
870      }
871
872      $(document).on("click", selector.stickyBtn, function () {
873        const checkin = $widget.find("#checkin").val();
874        const checkout = $widget.find("#checkout").val();
875
876        if ((checkin && checkout) || !settings.use_calendar) {
877          hkUIkit.modal(selector.stickyWidgetModal).show();
878        } else {
879          hkUIkit.modal(selector.calendarModal).show();
880        }
881      });
882    };
883
884    app.registerAddonActions = () => {
885      $widget.on("click", ".addon-increase-btn", function () {
886        const $input = $(this).prev();
887        const addonId = $input.data("id");
888        const addon = quote.available_addons.find(
889          (addon) => addon.id === addonId,
890        );
891
892        const selectedAddon = quote.selected_addons.find(
893          (item) => item.id === addonId,
894        );
895
896        let qty = 0;
897        if (selectedAddon) {
898          qty = parseInt(selectedAddon.quantity, 10);
899        }
900
901        if (qty < addon.max_qty) {
902          qty++;
903          $input.text(qty);
904
905          app.addAddon(quote.id, addonId, qty);
906        }
907      });
908
909      $widget.on("change", ".travel-insurance-input", function () {
910        app.setTravelInsurance(parseInt(this.value, 10) || 0);
911      });
912
913      $widget.on("click", ".addon-reduce-btn", function () {
914        const $input = $(this).next();
915        const addonId = parseInt($input.data("id"), 10);
916        const addon = quote.available_addons.find(
917          (addon) => addon.id === addonId,
918        );
919
920        const selectedAddon = quote.selected_addons.find(
921          (item) => item.id === addonId,
922        );
923
924        let qty = 0;
925        if (selectedAddon) {
926          qty = parseInt(selectedAddon.quantity, 10);
927        }
928
929        if (qty > 0) {
930          qty--;
931          $input.text(qty);
932
933          app.addAddon(quote.id, addonId, qty);
934        }
935
936        return false;
937      });
938    };
939
940    app.registerCouponActions = () => {
941      const applyCoupon = (e) => {
942        const coupon = $widget.find("#coupon").val() || options.coupon;
943
944        if (!quote.id) {
945          app.couponNotice(settings.coupon_require_dates, "warning");
946        } else if (!coupon) {
947          app.couponNotice(settings.empty_coupon_code, "warning");
948        } else {
949          app.applyCoupon(quote.id, coupon);
950        }
951
952        e.preventDefault();
953      };
954
955      $widget.on("click", "#apply-coupon-btn", applyCoupon);
956      // $widget.on("keypress", "#coupon", applyCoupon);
957      // listen for enter key
958      $widget.on("keypress", "#coupon", function (e) {
959        if (e.which === 13) {
960          applyCoupon(e);
961        }
962      });
963
964      $widget.on("change", "#have_coupon", function () {
965        if ($(this).is(":checked")) {
966          $widget.find(".coupon-input-row").show();
967        } else {
968          $widget.find(".coupon-input-row").hide();
969        }
970      });
971
972      if ($widget.find("#coupon").val()) {
973        $widget.find("#have_coupon").prop("checked", true).trigger("change");
974      }
975
976      // Add-ons (e.g. the toolkit Promo Code Chip) ask for the coupon to go.
977      // Central keeps a coupon on PUT with an empty value, so a fresh quote
978      // without the coupon is the only way to drop it (#877).
979      $widget.on("homelocal:coupon:remove", function () {
980        app.removeCoupon();
981      });
982    };
983
984    app.removeCoupon = () => {
985      if (!quote || !quote.id || !(quote.coupons || []).length) {
986        return false;
987      }
988
989      const { checkin, checkout, guests } = quote;
990
991      $widget.find("#coupon").val("");
992      $widget.find("#have_coupon").prop("checked", false).trigger("change");
993      // Otherwise the shortcode / URL coupon is re-applied on the next date change.
994      options.coupon = "";
995
996      pendingCouponEvent = "removed";
997      app.createQuote({ checkin, checkout, guests });
998
999      return true;
1000    };
1001
1002    // Fire homelocal:coupon:applied / homelocal:coupon:removed once the quote
1003    // that reflects the change has rendered, so listeners see final totals.
1004    app.fireCouponEvent = () => {
1005      const event = pendingCouponEvent;
1006      pendingCouponEvent = null;
1007
1008      if (!event) {
1009        return;
1010      }
1011
1012      if ("applied" === event) {
1013        const coupon = (quote.coupons || [])[0];
1014        if (!coupon) {
1015          return;
1016        }
1017
1018        $widget.trigger("homelocal:coupon:applied", [
1019          {
1020            code: coupon.code,
1021            type: coupon.type,
1022            discount: coupon.discount,
1023            quote,
1024          },
1025        ]);
1026      } else {
1027        $widget.trigger("homelocal:coupon:removed", [{ quote }]);
1028      }
1029    };
1030
1031    app.quoteNotice = (text = "", type = "primary") => {
1032      if (text) {
1033        $widget.find(selector.bookingOverview).hide();
1034        $widget
1035          .find(selector.quoteNotice)
1036          .html(`<div class="hk-alert hk-alert-${type}">${text}</div>`);
1037      } else {
1038        $widget.find(selector.bookingOverview).show();
1039        $widget.find(selector.quoteNotice).empty();
1040      }
1041    };
1042
1043    app.updateBookingCost = () => {
1044      const checkin = $widget.find("#checkin").val() || options.checkin;
1045      const checkout = $widget.find("#checkout").val() || options.checkout;
1046      const guests = $widget.find("#guests").val() || options.guests;
1047      const coupon = $widget.find("#coupon").val() || options.coupon;
1048
1049      if (!checkin || !checkout) {
1050        return;
1051      }
1052
1053      if (checkin && checkout && options.sticky) {
1054        app.updateStickyDates(checkin, checkout);
1055      }
1056
1057      if (!guests) {
1058        app.quoteNotice(settings.select_guests, "warning");
1059
1060        $widget.find(selector.widgetBody).show();
1061        $widget.find(selector.widgetLoader).hide();
1062
1063        $widget
1064          .find(selector.checkoutBtn)
1065          .prop("disabled", true)
1066          .text(settings.select_dates)
1067          .show();
1068        if (options.sticky) {
1069          $sticky
1070            .find(selector.stickyBtn)
1071            .prop("disabled", true)
1072            .text(settings.select_dates);
1073        }
1074      } else if (!calendarUtils.canCheckinout(checkin, checkout).status) {
1075        $widget.find(selector.widgetBody).show();
1076        $widget.find(selector.widgetLoader).hide();
1077
1078        $widget
1079          .find(selector.checkoutBtn)
1080          .prop("disabled", true)
1081          .text(settings.select_dates)
1082          .show();
1083        if (options.sticky) {
1084          $sticky
1085            .find(selector.stickyBtn)
1086            .prop("disabled", true)
1087            .text(settings.select_dates);
1088        }
1089
1090        app.quoteNotice(
1091          calendarUtils.canCheckinout(checkin, checkout).message,
1092          "warning",
1093        );
1094      } else if (checkin && checkout) {
1095        app.createQuote({ checkin, checkout, guests, coupon });
1096      }
1097    };
1098
1099    app.updateStickyPrice = () => {
1100      const checkin = quote.checkin;
1101      const checkout = quote.checkout;
1102      const rateplan = quote.rateplans.find(
1103        (item) => item.id === ratePlanSelected,
1104      );
1105
1106      const total = rateplan.total;
1107      const totalRent = rateplan.total_rent;
1108      const totalBeforeTax = rateplan.total - rateplan.total_taxes;
1109      const discount = rateplan.discount || 0;
1110      const totalNights = calendarUtils.calcNights(checkin, checkout);
1111
1112      let totalPrice = 0;
1113      if (options.total_after_tax) {
1114        totalPrice = total;
1115      } else {
1116        totalPrice = totalBeforeTax;
1117      }
1118
1119      let price = 0,
1120        actualPrice = parseFloat(totalPrice) / totalNights;
1121
1122      if ("average_price" === settings.price_with_dates) {
1123        actualPrice = parseFloat(totalPrice) / totalNights;
1124        price = parseFloat(totalPrice - discount) / totalNights;
1125      } else if ("average_rent" === settings.price_with_dates) {
1126        actualPrice = parseFloat(totalRent) / totalNights;
1127        price = parseFloat(totalRent - discount) / totalNights;
1128      }
1129
1130      $sticky.find(selector.stickyRentUnit).hide();
1131
1132      let rentText = `<span class="rent-after-date rent-amount">${app.price(
1133        price,
1134      )}</span>`;
1135
1136      if (actualPrice && actualPrice > price) {
1137        rentText = `<del class="rent-deleted">${app.price(
1138          actualPrice,
1139        )}</del><span class="rent-after-date rent-amount">${app.price(
1140          price,
1141        )}</span>`;
1142      }
1143
1144      const content = `
1145			<span class="hk-text-bold">
1146      ${settings.modal_rent_after_date_template.replace(
1147        "{price}",
1148        rentText,
1149      )} &bull; ${settings.modal_total_rent_template.replace(
1150        "{price}",
1151        `<span class="total-rent">${app.price(totalPrice)}</span>`,
1152      )}
1153      </span>
1154      <span class="hk-icon hk-position-relative" style="bottom:2px;">
1155        <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
1156          <path d="M12 19c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zm0-12.297c-2.913 0-5.297 2.384-5.297 5.297 0 2.914 2.384 5.297 5.297 5.297 2.914 0 5.297-2.383 5.297-5.297 0-2.913-2.383-5.297-5.297-5.297z" fill="#000"/>
1157          <path d="M11.129 10.45h1.703v5.448h-1.703v-5.449zM12.87 8.822a.87.87 0 1 1-1.741 0 .87.87 0 0 1 1.74 0z" fill="#000"/>
1158        </svg>
1159      </span>
1160			`;
1161
1162      if (options.price_modal) {
1163        $sticky
1164          .find(selector.stickyPrice)
1165          .html(
1166            `<a href="${selector.priceDetailsModal}" class="hk-link-reset hk-button-text hk-text-bold" hk-toggle>${content}</a>`,
1167          )
1168          .show();
1169      } else {
1170        $sticky
1171          .find(selector.stickyPrice)
1172          .html(
1173            `<span class="hk-link-reset hk-button-text hk-text-bold">${content}</span>`,
1174          )
1175          .show();
1176      }
1177    };
1178
1179    app.updateStickyDates = (checkin, checkout) => {
1180      const checkinDate = calendarUtils.getDateZero(checkin);
1181      const checkoutDate = calendarUtils.getDateZero(checkout);
1182      let dateStr = `${checkinDate.toFormat("MMM d")}
1182 - ${checkoutDate.toFormat(
1183        "MMM d",
1184      )}`;
1185
1186      $sticky.find(selector.stickyRentUnit).hide();
1187
1188      $sticky
1189        .find(selector.stickyDates)
1190        .html(
1191          `
1192       <a href="${
1193         settings.use_calendar
1194           ? selector.calendarModal
1195           : selector.stickyWidgetModal
1196       }" class="hk-link-reset hk-button-text hk-flex-inline hk-flex-center hk-flex-middle"" hk-toggle>
1197        <span>${dateStr}</span>
1198        <span class="hk-margin-small-left hk-icon">
1199          <svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
1200            <path d="M8 4.8a.8.8 0 0 0-1.6 0v.8A2.4 2.4 0 0 0 4 8v9.6A2.4 2.4 0 0 0 6.4 20h3.2v-1.6H6.4a.8.8 0 0 1-.8-.8v-7.2h12.8V8A2.4 2.4 0 0 0 16 5.6v-.8a.8.8 0 0 0-1.6 0v.8H8v-.8z" fill="#000"/>
1201            <path d="M8 12a.8.8 0 0 0 0 1.6h4a.8.8 0 0 0 0-1.6H8zM7.2 16.001a.8.8 0 0 1 .8-.8h1.6a.8.8 0 0 1 0 1.6H8a.8.8 0 0 1-.8-.8z" fill="#000"/>
1202            <path fill-rule="evenodd" clip-rule="evenodd" d="M18.165 11.434a.8.8 0 0 0-1.131 0l-5.2 5.2a.8.8 0 0 0-.22.409l-.4 2a.8.8 0 0 0 .942.94l2-.4a.8.8 0 0 0 .409-.218l5.2-5.2a.8.8 0 0 0 0-1.131l-1.6-1.6zm-5.029 6.16L16.4 14.33l.469.468-3.263 3.263-.586.117.117-.585z" fill="#000"/>
1203          </svg>
1204        </span>
1205       </a>
1206			`,
1207        )
1208        .show();
1209    };
1210
1211    app.updatePriceBreakdown = () => {
1212      const rateplan = quote.rateplans.find(
1213        (item) => item.id === ratePlanSelected,
1214      );
1215
1216      const total = rateplan.total;
1217      const discount = rateplan.discount || 0;
1218      const totalRent = rateplan.total_rent;
1219      const totalBeforeTax = rateplan.total - rateplan.total_taxes;
1220      const discountLineItems = rateplan.invoice_items.filter((item) =>
1221        ["PROMOTION", "DISCOUNT"].includes(item.type),
1222      );
1223
1224      app.renderTotal(total, totalBeforeTax, discountLineItems);
1225
1226      // const checkin = quote.checkin;
1227      // const checkout = quote.checkout;
1228      // const totalNights = calendarUtils.calcNights(checkin, checkout);
1229      const totalNights = quote.nights;
1230
1231      app.renderNights(totalNights);
1232
1233      if (options.show_rent) {
1234        if ("average_price" === settings.price_with_dates) {
1235          if (options.total_after_tax) {
1236            app.renderRentAfterDate(total, totalNights, discount);
1237          } else {
1238            app.renderRentAfterDate(totalBeforeTax, totalNights, discount);
1239          }
1240        } else if ("average_rent" === settings.price_with_dates) {
1241          app.renderRentAfterDate(totalRent, totalNights, discount);
1242        }
1243      }
1244
1245      if (options.sticky) {
1246        app.updateStickyPrice();
1247        app.updateStickyDates(quote.checkin, quote.checkout);
1248      }
1249
1250      if (options.price_modal) {
1251        app.updatePriceDetailsModal(rateplan, total, totalBeforeTax);
1252      }
1253    };
1254
1255    app.updatePriceDetailsModal = (rateplan, total, subTotal = 0) => {
1256      const $modal = $(selector.priceDetailsModal);
1257      const $tbody = $modal.find("table tbody");
1258      $tbody.empty();
1259
1260      let rent_rows = "",
1261        fee_rows = "",
1262        manual_rows = "",
1263        tax_rows = "";
1264      refundable_rows = "";
1265
1266      rateplan.invoice_items.forEach(function (item) {
1267        let label = item.title;
1268        if (settings.rent_item_types.includes(item.type)) {
1269          if (!rent_rows) {
1270            rent_rows += `<tr>
1271            <td>${settings.rent_group_label}</td>
1272            <td>${label}</td>
1273            <td class="hk-text-right">${app.price(item.amount)}</td>
1274          </tr>`;
1275          } else {
1276            rent_rows += `<tr>
1277            <td></td>
1278            <td>${label}</td>
1279            <td class="hk-text-right">${app.price(item.amount)}</td>
1280          </tr>`;
1281          }
1282        }
1283
1284        if (settings.fee_item_types.includes(item.type)) {
1285          if (!fee_rows) {
1286            fee_rows += `<tr>
1287            <td>${settings.fees_group_label}</td>
1288            <td>${label}</td>
1289            <td class="hk-text-right">${app.price(item.amount)}</td>
1290          </tr>`;
1291          } else {
1292            fee_rows += `<tr>
1293            <td></td>
1294            <td>${label}</td>
1295            <td class="hk-text-right">${app.price(item.amount)}</td>
1296          </tr>`;
1297          }
1298        }
1299
1300        //manual_rows
1301        if (settings.manual_item_types.includes(item.type)) {
1302          if (!manual_rows) {
1303            manual_rows += `<tr>
1304            <td>${settings.manual_group_label}</td>
1305            <td>${label}</td>
1306            <td class="hk-text-right">${app.price(item.amount)}</td>
1307          </tr>`;
1308          } else {
1309            manual_rows += `<tr>
1310            <td></td>
1311            <td>${label}</td>
1312            <td class="hk-text-right">${app.price(item.amount)}</td>
1313          </tr>`;
1314          }
1315        }
1316
1317        if (settings.tax_item_types.includes(item.type)) {
1318          if (!tax_rows) {
1319            tax_rows += `<tr>
1320            <td>${settings.taxes_group_label}</td>
1321            <td>${label}</td>
1322            <td class="hk-text-right">${app.price(item.amount)}</td>
1323          </tr>`;
1324          } else {
1325            tax_rows += `<tr>
1326            <td></td>
1327            <td>${label}</td>
1328            <td class="hk-text-right">${app.price(item.amount)}</td>
1329          </tr>`;
1330          }
1331        }
1332
1333        if (settings.refundable_item_types.includes(item.type)) {
1334          if (!refundable_rows) {
1335            refundable_rows += `<tr>
1336            <td>${settings.others_group_label}</td>
1337            <td>${label}</td>
1338            <td class="hk-text-right">${app.price(item.amount)}</td>
1339          </tr>`;
1340          } else {
1341            refundable_rows += `<tr>
1342            <td></td>
1343            <td>${label}</td>
1344            <td class="hk-text-right">${app.price(item.amount)}</td>
1345          </tr>`;
1346          }
1347        }
1348      });
1349
1350      $tbody.append(rent_rows);
1351      $tbody.append(fee_rows);
1352      if (manual_rows) {
1353        $tbody.append(manual_rows);
1354      }
1355      if (refundable_rows) {
1356        $tbody.append(refundable_rows);
1357      }
1358
1359      if ("1" === settings.show_subtotal && subTotal) {
1360        $tbody.append(`
1361        <tr class="hk-background-muted">
1362          <td class="hk-text-bold">${settings.subtotal_group_label}</td>
1363          <td></td>
1364          <td class="hk-text-right hk-text-bold">${app.price(subTotal)}</td>
1365        </tr>`);
1366      }
1367
1368      $tbody.append(tax_rows);
1369
1370      $tbody.append(`
1371        <tr class="hk-background-muted">
1372          <td class="hk-text-bold">${settings.total_group_label}</td>
1373          <td></td>
1374          <td class="hk-text-right hk-text-bold">${app.price(total)}</td>
1375        </tr>`);
1376    };
1377
1378    app.createQuote = ({ checkin, checkout, guests, coupon }) => {
1379      $widget.find(selector.bookingOverview).hide();
1380      $widget.find(selector.quoteNotice).empty();
1381      $widget.find(".discount-items").hide();
1382      $widget.find(".coupon-row").hide();
1383      $widget.find(".rateplans").hide();
1384      $widget.find(selector.priceLoader).show();
1385
1386      $widget.trigger("updating");
1387
1388      const totalNights = calendarUtils.calcNights(checkin, checkout);
1389      app.renderNights(totalNights);
1390
1391      $.post(`${settings.plugin_api}quotes`, {
1392        checkin,
1393        checkout,
1394        guests,
1395        coupon,
1396        property_id: options.property_id,
1397        traffic_source: options.traffic_source,
1398      })
1399        .done(function (response) {
1400          $widget.find(selector.bookingOverview).show();
1401
1402          // store quote data in cache.
1403          quote = response;
1404          if (!quote.rateplans) {
1405            quote.rateplans = [];
1406          }
1407
1408          app.renderRateplans();
1409
1410          if (quote.available_addons) {
1411            app.renderAddons();
1412          }
1413
1414          if (options.show_coupon) {
1415            app.renderCoupon();
1416          }
1417
1418          app.fireCouponEvent();
1419
1420          $widget.trigger("available");
1421        })
1422        .fail(function (xhr) {
1423          pendingCouponEvent = null;
1424
1425          if (options.show_rent) {
1426            app.hideRent();
1427          }
1428
1429          let error = settings.internal_error;
1430          if (xhr.responseJSON && xhr.responseJSON.message) {
1431            error = xhr.responseJSON.message;
1432          }
1433
1434          app.quoteNotice(error, "danger");
1435
1436          if (options.show_coupon && coupon) {
1437            $widget.find(".coupon-row").show();
1438          }
1439
1440          $widget.trigger("unavailable", [error]);
1441        })
1442        .always(function () {
1443          $widget.find(selector.widgetBody).show();
1444          $widget.find(selector.widgetLoader).hide();
1445          $widget.find(selector.priceLoader).hide();
1446        });
1447    };
1448
1449    app.renderRateplans = () => {
1450      const $rateplans = $widget.find(".rateplans");
1451      $rateplans.empty();
1452
1453      if (quote.rateplans.length > 1) {
1454        quote.rateplans.forEach((item) => {
1455          let prText = "";
1456
1457          if (item.priceAdjustment) {
1458            const prAd = item.priceAdjustment;
1459            if (prAd.amount !== 0) {
1460              if ("increase" === prAd.direction) {
1461                if (prAd.type === "percent") {
1462                  prText = `+${prAd.amount}%`;
1463                } else {
1464                  prText = "+" + app.price(prAd.amount);
1465                }
1466              } else {
1467                if (prAd.type === "percent") {
1468                  prText = `-${prAd.amount}%`;
1469                } else {
1470                  prText = "-" + app.price(prAd.amount);
1471                }
1472              }
1473
1474              if (prText) {
1475                prText = ` (${prText})`;
1476              }
1477            }
1478          }
1479
1480          $rateplans.append(`
1481              <label class="hk-flex hk-flex-middle hk-margin-small rateplan">
1482                <span class="hk-flex-none">
1483                  <input type="radio" name="rateplan" class="hk-radio" value="${
1484                    item.id
1485                  }" />
1486                </span>
1487                <span class="hk-flex-auto hk-margin-small-left">
1488                  <div><span class="hk-text-bold">${
1489                    item.title || item.name
1490                  }</span>${prText}</div>
1491                  ${
1492                    item.description
1493                      ? `<div class="hk-text-small">${item.description}</div>`
1494                      : ""
1495                  }
1496                </span>
1497              </label>
1498            `);
1499        });
1500
1501        $rateplans.show();
1502
1503        if (ratePlanSelected) {
1504          $rateplans
1505            .find(`input[value="${ratePlanSelected}"]`)
1506            .prop("checked", true)
1507            .trigger("change");
1508        } else {
1509          $rateplans
1510            .find("input:first")
1511            .prop("checked", true)
1512            .trigger("change");
1513        }
1514      } else if (quote.rateplans.length === 1) {
1515        $rateplans.append(
1516          `<input type="hidden" name="rateplan" class="hk-radio" value="${quote.rateplans[0].id}" />`,
1517        );
1518        $rateplans.find("input:first").trigger("change");
1519      }
1520    };
1521
1522    app.rateplanHasStayDiscount = () => {
1523      if (!quote || !Array.isArray(quote.rateplans)) {
1524        return false;
1525      }
1526
1527      const rateplan = quote.rateplans.find(
1528        (item) => item.id === ratePlanSelected,
1529      );
1530
1531      if (!rateplan || !Array.isArray(rateplan.invoice_items)) {
1532        return false;
1533      }
1534
1535      return rateplan.invoice_items.some(
1536        (item) => "STAY_DISCOUNT" === item.type,
1537      );
1538    };
1539
1540    app.shouldHideCoupon = () => {
1541      if (!options.hide_coupon_on_stay_discount) {
1542        return false;
1543      }
1544
1545      // Never hide an applied coupon's state (e.g. coupon via URL).
1546      if (quote && quote.coupons && quote.coupons.length > 0) {
1547        return false;
1548      }
1549
1550      return app.rateplanHasStayDiscount();
1551    };
1552
1553    app.renderCoupon = () => {
1554      if (app.shouldHideCoupon()) {
1555        $widget.find(".coupon-row").hide();
1556        app.couponNotice();
1557        return;
1558      }
1559
1560      $widget.find(".coupon-row").show();
1561
1562      if (quote.coupons && quote.coupons.length > 0) {
1563        const coupon = quote.coupons[0];
1564
1565        let discountText = "";
1566
1567        if (coupon.type === "PERCENT") {
1568          discountText = `${coupon.discount}%`;
1569        } else {
1570          discountText = app.price(Math.abs(coupon.discount));
1571        }
1572
1573        $widget.find("#have_coupon").prop("checked", true).trigger("change");
1574        $widget.find("#coupon").val(coupon.code);
1575        app.couponNotice(
1576          settings.coupon_discount_applied.replace("{amount}", discountText),
1577          "success",
1578        );
1579      } else {
1580        $widget.find("#have_coupon").prop("checked", false).trigger("change");
1581        $widget.find("#coupon").val("");
1582        app.couponNotice();
1583      }
1584    };
1585
1586    app.couponNotice = (text = "", type = "primary") => {
1587      if (text) {
1588        $widget
1589          .find(selector.couponNotice)
1590          .html(`<div class="hk-alert hk-alert-${type}">${text}</div>`);
1591      } else {
1592        $widget.find(selector.couponNotice).empty();
1593      }
1594    };
1595
1596    app.travelInsuranceNotice = (text = "", type = "primary") => {
1597      const $notice = $widget.find(".travel-insurance-notice");
1598
1599      if (text) {
1600        $notice.html(`<div class="hk-alert hk-alert-${type}">${text}</div>`);
1601      } else {
1602        $notice.empty();
1603      }
1604    };
1605
1606    app.removeLangFromUrl = (url) => {
1607      const parsed = new URL(url);
1608
1609      // 1. Force base hostname without language subdomains
1610      // If hostname is like fr.fridaysflats.com → take the main domain
1611      const hostParts = parsed.hostname.split(".");
1612      if (hostParts.length > 2) {
1613        // remove first part only if it's a language code
1614        const possibleLang = hostParts[0];
1615        if (settings.available_languages.includes(possibleLang)) {
1616          hostParts.shift();
1617          parsed.hostname = hostParts.join(".");
1618        }
1619      }
1620
1621      // 2. Remove `/xx/` language folder before `/wp-json/`
1622      const segments = parsed.pathname.split("/").filter(Boolean);
1623      const hasTrailingSlash = parsed.pathname.endsWith("/");
1624
1625      if (segments.length >= 2) {
1626        const first = segments[0];
1627        if (settings.available_languages.includes(first)) {
1628          segments.shift(); // remove language
1629        }
1630      }
1631
1632      parsed.pathname = "/" + segments.join("/");
1633      if (hasTrailingSlash) {
1634        parsed.pathname += "/";
1635      }
1636
1637      return parsed.toString();
1638    };
1639
1640    app.applyCoupon = (quoteId, coupon) => {
1641      $widget.trigger("updating");
1642
1643      app.couponNotice("Applying coupon, please wait.", "warning");
1644      $widget.find("#apply-coupon-btn").prop("disabled", true);
1645
1646      // @see https://github.com/hudsoncreativestudio/homerunner-plugin/issues/549
1647      let url = `${settings.plugin_api}quotes/${quoteId}`;
1648      if ("1" === settings.force_direct_quote_endpoint) {
1649        // Remove language code from URL (subdomain and path) when using direct endpoint
1650        url = app.removeLangFromUrl(url);
1651      }
1652
1653      $.ajax({
1654        method: "PUT",
1655        url: url,
1656        data: {
1657          coupon,
1658        },
1659      })
1660        .done(function () {
1661          app.couponNotice("Coupon Applied.", "warning");
1662
1663          if ("1" === options.coupon_reload) {
1664            window.location.reload();
1665          } else {
1666            pendingCouponEvent = "applied";
1667            app.refreshQuote();
1668          }
1669        })
1670        .fail(function (xhr) {
1671          if (xhr.responseJSON && xhr.responseJSON.message) {
1672            app.couponNotice(xhr.responseJSON.message, "danger");
1673          } else {
1674            app.couponNotice(settings.internal_error, "danger");
1675          }
1676          $widget.trigger("available");
1677        })
1678        .always(function () {
1679          $widget.find("#apply-coupon-btn").prop("disabled", false);
1680        });
1681    };
1682
1683    app.refreshQuote = (id = null) => {
1684      if (!id) {
1685        id = quote.id;
1686      }
1687
1688      $widget.find(selector.bookingOverview).hide();
1689      $widget.find(selector.priceLoader).show();
1690      $widget.trigger("updating");
1691
1692      $.get(`${settings.plugin_api}quotes/${id}?_=${new Date().getTime()}`)
1693        .done(function (response) {
1694          $widget.find(selector.quoteNotice).empty();
1695          $widget.find(selector.bookingOverview).show();
1696
1697          // store quote data in cache.
1698          quote = response;
1699          if (!quote.rateplans) {
1700            quote.rateplans = [];
1701          }
1702
1703          if ($widget.find("#checkin").length > 0) {
1704            $widget.find("#checkin").val(quote.checkin);
1705          }
1706          if ($widget.find("#checkout").length > 0) {
1707            $widget.find("#checkout").val(quote.checkout);
1708          }
1709          if ($widget.find("#guests").length > 0) {
1710            $widget.find("#guests").val(quote.guests);
1711          }
1712
1713          app.renderRateplans();
1714          app.renderCoupon();
1715
1716          if (quote.available_addons) {
1717            app.renderAddons();
1718          }
1719
1720          app.fireCouponEvent();
1721
1722          $widget.trigger("available");
1723        })
1724        .fail(function (xhr) {
1725          pendingCouponEvent = null;
1726
1727          if (options.show_rent) {
1728            app.hideRent();
1729          }
1730
1731          if (xhr.responseJSON && xhr.responseJSON.message) {
1732            app.quoteNotice(xhr.responseJSON.message, "warning");
1733          } else {
1734            app.quoteNotice(settings.internal_error, "warning");
1735          }
1736
1737          $widget.trigger("unavailable");
1738        })
1739        .always(function () {
1740          $widget.find(selector.widgetBody).show();
1741          $widget.find(selector.widgetLoader).hide();
1742          $widget.find(selector.priceLoader).hide();
1743        });
1744    };
1745
1746    app.postAddon = (quoteId, addonId, quantity) => {
1747      return $.ajax({
1748        method: "POST",
1749        url: `${settings.plugin_api}quotes/${quoteId}/addons`,
1750        data: {
1751          addon_id: addonId,
1752          quantity: quantity,
1753        },
1754      });
1755    };
1756
1757    app.addAddon = (quoteId, addonId, quantity) => {
1758      $widget.trigger("updating");
1759
1760      app.couponNotice(settings.updating_addon, "warning");
1761
1762      app
1763        .postAddon(quoteId, addonId, quantity)
1764        .done(function () {
1765          app.couponNotice(settings.addon_updated, "success");
1766          app.refreshQuote();
1767        })
1768        .fail(function (xhr) {
1769          if (xhr.responseJSON && xhr.responseJSON.message) {
1770            app.couponNotice(xhr.responseJSON.message, "danger");
1771          } else {
1772            app.couponNotice(settings.internal_error, "danger");
1773          }
1774          $widget.trigger("available");
1775        })
1776        .always(function () {});
1777    };
1778
1779    app.isTravelCoverage = (item) => {
1780      return (
1781        !!item &&
1782        typeof item.target_fee === "string" &&
1783        travelCoverageFeeTypes.indexOf(item.target_fee) !== -1
1784      );
1785    };
1786
1787    /**
1788     * Addons flagged `is_visible: false` stay in the quote but are never listed.
1789     */
1790    app.isAddonVisible = (item) => {
1791      return !!item && false !== item.is_visible;
1792    };
1793
1794    app.addonPrice = (item) => {
1795      return typeof item.amount_type === "string" &&
1796        "percentage" === item.amount_type
1797        ? item.amount + "%"
1798        : app.price(item.amount);
1799    };
1800
1801    /**
1802     * Amount an addon costs against the selected rate plan, null when the quote
1803     * does not carry the computed map (older cached quote bodies).
1804     */
1805    app.rateplanAddonAmount = (item) => {
1806      const rateplans = quote.rateplans || [];
1807      const rateplan =
1808        rateplans.find((plan) => plan.id === ratePlanSelected) || rateplans[0];
1809      const amounts = (rateplan && rateplan.addon_amounts) || {};
1810      const amount = amounts[item.id];
1811
1812      return amount === undefined || amount === null ? null : parseFloat(amount);
1813    };
1814
1815    /**
1816     * Id of the travel coverage addon currently applied to the quote, 0 when none.
1817     */
1818    app.selectedTravelCoverageId = (coverage) => {
1819      const selected = (quote.selected_addons || []).find((item) => {
1820        return (
1821          coverage.some((addon) => addon.id === item.id) &&
1822          parseInt(item.quantity, 10) > 0
1823        );
1824      });
1825
1826      return selected ? selected.id : 0;
1827    };
1828
1829    /**
1830     * Travel coverage always shows a currency amount, never the raw percentage.
1831     */
1832    app.travelCoveragePrice = (item) => {
1833      const amount = app.rateplanAddonAmount(item);
1834
1835      return null === amount ? app.addonPrice(item) : app.price(amount);
1836    };
1837
1838    /**
1839     * Travel coverage rows render as an exclusive radio group, not as steppers.
1840     */
1841    app.renderTravelInsurance = (coverage) => {
1842      const $section = $widget.find(".travel-insurance");
1843
1844      if (!$section.length) {
1845        return;
1846      }
1847
1848      $section.find(".travel-insurance-items").empty();
1849
1850      if (!coverage.length || "1" !== options.show_addons) {
1851        $section.hide();
1852        return;
1853      }
1854
1855      $section.show();
1856
1857      const selectedId = app.selectedTravelCoverageId(coverage);
1858
1859      coverage.forEach((item) => {
1860        $section.find(".travel-insurance-items").append(`
1861					<label class="travel-insurance-option">
1862						<input class="travel-insurance-input" type="radio" name="${travelInsuranceGroup}"
1863							value="${item.id}" ${selectedId === item.id ? "checked" : ""} />
1864						<span class="travel-insurance-label">
1865							<span class="travel-insurance-name">${item.name}</span>
1866							${
1867                item.description && item.description !== item.name
1868                  ? `<span class="travel-insurance-description">${item.description}</span>`
1869                  : ""
1870              }
1871						</span>
1872						<span class="travel-insurance-price">${app.travelCoveragePrice(
1873              item,
1874            )}</span>
1875					</label>
1876				`);
1877      });
1878
1879      $section.find(".travel-insurance-items").append(`
1880				<label class="travel-insurance-option">
1881					<input class="travel-insurance-input" type="radio" name="${travelInsuranceGroup}"
1882						value="0" ${selectedId ? "" : "checked"} />
1883					<span class="travel-insurance-label">
1884						<span class="travel-insurance-name">${settings.travel_insurance_none}</span>
1885					</span>
1886				</label>
1887			`);
1888    };
1889
1890    /**
1891     * Apply a travel coverage addon, removing the previously selected one first.
1892     * `addonId` 0 means "no insurance".
1893     */
1894    app.setTravelInsurance = (addonId) => {
1895      const coverage = (quote.available_addons || []).filter(
1896        app.isTravelCoverage,
1897      );
1898      const currentId = app.selectedTravelCoverageId(coverage);
1899
1900      if (currentId === addonId) {
1901        return;
1902      }
1903
1904      $widget.find(".travel-insurance-input").prop("disabled", true);
1905      $widget.trigger("updating");
1906      app.travelInsuranceNotice(
1907        addonId
1908          ? settings.adding_travel_insurance
1909          : settings.removing_travel_insurance,
1910        "warning",
1911      );
1912
1913      const onFail = (xhr) => {
1914        if (xhr.responseJSON && xhr.responseJSON.message) {
1915          app.travelInsuranceNotice(xhr.responseJSON.message, "danger");
1916        } else {
1917          app.travelInsuranceNotice(settings.internal_error, "danger");
1918        }
1919
1920        // Re-read the quote so the radios match what is actually applied.
1921        app.refreshQuote();
1922      };
1923
1924      const applySelected = () => {
1925        if (!addonId) {
1926          app.travelInsuranceNotice();
1927          app.refreshQuote();
1928          return;
1929        }
1930
1931        app
1932          .postAddon(quote.id, addonId, 1)
1933          .done(() => {
1934            app.travelInsuranceNotice();
1935            app.refreshQuote();
1936          })
1937          .fail(onFail);
1938      };
1939
1940      if (currentId) {
1941        app.postAddon(quote.id, currentId, 0).done(applySelected).fail(onFail);
1942      } else {
1943        applySelected();
1944      }
1945    };
1946
1947    app.renderAddons = () => {
1948      $widget.find(".addon-items").empty();
1949
1950      const available = (quote.available_addons || []).filter(
1951        app.isAddonVisible,
1952      );
1953      const coverage = available.filter(app.isTravelCoverage);
1954      const addonItems = available.filter((item) => !app.isTravelCoverage(item));
1955
1956      app.renderTravelInsurance(coverage);
1957
1958      // return if empty.
1959      if (!addonItems.length) {
1960        $widget.find(".addons").hide();
1961        return;
1962      }
1963
1964      if ("1" !== options.show_addons) {
1965        $widget.find(".addons").hide();
1966        return;
1967      }
1968
1969      $widget.find(".addons").show();
1970
1971      addonItems.forEach((item) => {
1972        let qty = 0;
1973        const isAdded = quote.selected_addons.find(
1974          (addon) => addon.id === item.id,
1975        );
1976        if (isAdded) {
1977          qty = isAdded.quantity;
1978        }
1979
1980        const min = 0;
1981        const max = item.max_qty || 1;
1982        const price = app.addonPrice(item);
1983
1984        $widget.find(".addon-items").append(`
1985					<div class="addon">
1986						<a class="hk-link-text addon-name" href="#" role="button">${item.name}</a>
1987						<div hk-drop="pos: left-center; auto-update: false;" class="addon-description-container">
1988						<div class="hk-card hk-card-default hk-card-small hk-card-body hk-margin-remove-first-child addon-description">
1989						<div class="hk-panel hk-margin-top">${item.description || ""}</div>
1990						</div>
1991						</div>
1992						<span class="addon-price">${price}</span>
1993						<span class="addon-control">
1994							<button class="addon-reduce-btn" type="button" ${min === qty ? "disabled" : ""}>
1995								<span hk-icon="icon: minus; ratio: .5"></span>
1996							</button>
1997							<span class="addon-input" data-id="${item.id}" >${qty}</span>
1998							<button class="addon-increase-btn" type="button" ${
1999                max === qty ? "disabled" : ""
2000              }>
2001								<span hk-icon="icon: plus; ratio: .5"></span>
2002							</button>
2003						</span>
2004					</div>
2005				`);
2006      });
2007    };
2008
2009    app.price = (price) => {
2010      return $.homelocalPrice(price, options.currency, false);
2011    };
2012
2013    app.getFormattedDate = (element) => {
2014      var date;
2015      try {
2016        date = element.value;
2017      } catch (error) {
2018        date = null;
2019      }
2020
2021      return date;
2022    };
2023
2024    app.resetWidget = () => {
2025      quote = null;
2026
2027      $widget.find(selector.bookingOverview).hide();
2028      $widget.find(selector.quoteNotice).empty();
2029      $widget.find("#checkin,#checkin_placeholder").val("");
2030      $widget.find("#checkout,#checkout_placeholder").val("");
2031      $widget.find(".discount-items").hide();
2032      $widget.find(".coupon-row").hide();
2033      $widget.find(".rateplans").hide();
2034
2035      $widget
2036        .find(selector.checkoutBtn)
2037        .prop("disabled", false)
2038        .text(settings.select_dates)
2039        .show();
2040
2041      if (options.sticky) {
2042        $sticky
2043          .find(selector.stickyBtn)
2044          .prop("disabled", false)
2045          .text(settings.select_dates);
2046        $sticky.find(selector.stickyTotalNights).show();
2047        $sticky.find(selector.stickyDates).hide();
2048        $sticky.find(selector.stickyPrice).hide();
2049        $sticky.find(selector.stickyNotice).empty().hide();
2050      }
2051
2052      app.showNoDatesRent();
2053
2054      if (options.show_min_stay) {
2055        app.renderMinimumNights();
2056      } else {
2057        app.hideMinimumNights();
2058      }
2059
2060      app.updateUrl();
2061    };
2062
2063    app.init();
2064
2065    // Public API for add-on scripts: $widget.data("homelocalBookingWidget").removeCoupon()
2066    $widget.data("homelocalBookingWidget", app);
2067
2068    $(document).on("homelocal_booking_widget_reset", app.resetWidget);
2069
2070    // update nights when checkin selected.
2071    $(document).on(
2072      "homelocal_availability_calendar_set_checkin_" + options.property_id,
2073      function (e, day) {
2074        if (options.show_min_stay) {
2075          setTimeout(function () {
2076            app.renderMinimumNights(calendarUtils.getMinstay(day));
2077          }, 2);
2078        }
2079      },
2080    );
2081  };
2082})(jQuery, homelocal_booking_widget_settings);

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.