1var kombi = { 2 getOnlyDigits: function (text) { 3 return text.replace(/\D/g, ""); 4 }, 5 6 getCheckoutCartId: function () { 7 var byId = document.getElementById("cart_id"); 8 if (byId && byId.value) { 9 return byId.value; 10 } 11 var byClass = document.querySelector("input.cart_id"); 12 if (byClass && byClass.value) { 13 return byClass.value; 14 } 15 var match = window.location.pathname.match( 16 /\/(?:one-page-checkout|payment|travelers|quotation)\/([0-9a-fA-F-]{36})(?:\/|$)/ 17 ); 18 return match ? match[1] : ""; 19 }, 20 21 getBoolAttr: function (el, attr, _default) { 22 let value = el.getAttribute(attr); 23 if (value !== null) { 24 return value.toLowerCase() == "true"; 25 } 26 return _default; 27 }, 28 29 disableOnClick: function (el) { 30 if (el.tagName === "A") { 31 el.classList.add("disabled"); 32 el.style.pointerEvents = "none"; 33 } else { 34 el.disabled = true; 35 } 36 }, 37 38 initTooltips: function (container) { 39 if (typeof bootstrap === "undefined" || !bootstrap.Tooltip) return; 40 const root = container || document; 41 const trigger = "hover focus click"; 42 root.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) { 43 const existing = bootstrap.Tooltip.getInstance(el); 44 if (existing) existing.dispose(); 45 new bootstrap.Tooltip(el, { trigger: trigger }); 46 }); 47 }, 48}; 49 50document.addEventListener("click", function (evt) { 51 if (typeof bootstrap === "undefined" || !bootstrap.Tooltip) return; 52 const clickedTrigger = evt.target.closest("[data-bs-toggle='tooltip']"); 53 const clickedInsideTooltip = evt.target.closest(".tooltip"); 54 55 if (!clickedTrigger && !clickedInsideTooltip) { 56 document.querySelectorAll("[data-bs-toggle='tooltip']").forEach(function (el) { 57 const t = bootstrap.Tooltip.getInstance(el); 58 if (t) t.hide(); 59 }); 60 } else if (clickedTrigger) { 61 document.querySelectorAll("[data-bs-toggle='tooltip']").forEach(function (el) { 62 if (el !== clickedTrigger) { 63 const t = bootstrap.Tooltip.getInstance(el); 64 if (t) t.hide(); 65 } 66 }); 67 } 68}, true); 69 70document.addEventListener("DOMContentLoaded", function () { 71 kombi.searchTabs(); 72 kombi.search(); 73 kombi.searchBar(); 74 kombi.flatpickr(); 75 if (kombi.calendarMobile) kombi.calendarMobile(); 76 if (kombi.cleaveMask) kombi.cleaveMask(); // ausente no hotel-detail bundle 77 kombi.activeLinks(); 78 if (kombi.paymentPagBank) kombi.paymentPagBank(); // ausente no hotel-detail bundle 79 kombi.disableCollapseFields(); 80 if (kombi.travelersFormValidation) kombi.travelersFormValidation(); // ausente no hotel-detail bundle 81 if (kombi.checkoutTimer) { // ausente no hotel-detail bundle 82 kombi.checkoutTimer.checkAvailability(); 83 kombi.checkoutTimer.fixedDiv(); 84 } 85 kombi.carousel(); 86 if (kombi.reservation) kombi.reservation.navLinkActiveStyle(); // ausente no hotel-detail bundle 87 if (kombi.profile) kombi.profile.formatPhoneNumber(); // ausente no hotel-detail bundle 88 kombi.sidebar.collapseToggle(); 89 kombi.sidebar.menuLinkActive(); 90 kombi.sidebar.mobileDropdownModal(); 91 kombi.sidebar.navBarFade(); 92 kombi.toggleReadMore.init(); 93 kombi.readMore(); 94 kombi.flight.bindDeltaPricing(); 95 kombi.flight.bindPackageCheckoutVisibility(); 96 kombi.flight.initPackageCheckoutVisibility(); 97 kombi.flight.calculateDeltaPricing(); 98 kombi.flight.changeDateMobile(); 99 if (kombi.reloadInstallments) kombi.reloadInstallments(); // ausente no hotel-detail bundle 100 window.onload = kombi.flight.changeDateMobile; 101 window.onresize = kombi.flight.changeDateMobile; 102 kombi.flight.changeDateDesktop(); 103 kombi.accordion(); 104 kombi.hotel(); 105 kombi.packageSearchPollingLoader(); 106 kombi.checkSearchHash(); 107 kombi.travelSuitcaseNavLinkMobile(); 108 kombi.travelSuitcaseNavLinkDesktop(); 109 kombi.initTooltips(); 110}); 111 112// HTMX event guide (this project): 113// - afterOnLoad: UI reinits on new content (amenities, rooms, prices) 114// - afterSwap: one-shot body listeners via kombi.lifecycle.bindOnce / onBody 115// - afterSettle: settled DOM â tooltips, analytics 116const HTMX_AFTER_ON_LOAD = [ 117 { run: function (root) { kombi.toggleReadMore.init(root); } }, 118 { run: function (root) { kombi.readMore(root); } }, 119 { run: function (root) { kombi.carousel(root); } }, 120 { run: function () { kombi.flight.initPackageCheckoutVisibility(); } }, 121 { run: function () { kombi.flight.calculateDeltaPricing(); } }, 122 { run: function (root) { kombi.hotel(root); } }, 123 { run: function () { kombi.packageSearchPollingLoader(); } }, 124 { run: function () { kombi.showCartToastIfFull(); } }, 125]; 126 127document.body.addEventListener("htmx:afterOnLoad", function (event) { 128 const root = event.target; 129 HTMX_AFTER_ON_LOAD.forEach(function (entry) { 130 entry.run(root); 131 }); 132}); 133 134document.body.addEventListener("htmx:afterSettle", function (event) { 135 kombi.initTooltips(event.target); 136}); 137 138const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]'); 139const popoverList = [...popoverTriggerList].map(function (popoverTriggerEl) { 140 return new bootstrap.Popover(popoverTriggerEl); 141}); 142; 143/** 144 * Shared HTMX lifecycle helpers (bind-once + per-element init guards). 145 * Consumed by storefront modules after DOMContentLoaded / htmx swaps. 146 */ 147kombi.lifecycle = (function () { 148 const onceKeys = Object.create(null); 149 const DATASET_PREFIX = "kombiInit"; 150 const VALID_KEY = /^[a-zA-Z0-9_-]+$/; 151 152 function isNonEmptyString(value) { 153 return typeof value === "string" && value.length > 0; 154 } 155 156 function hasDataset(el) { 157 return !!(el && el.dataset); 158 } 159 160 /** 161 * "room-toggle" â "kombiInitRoomToggle" (dataset / data-kombi-init-room-toggle). 162 */ 163 function toDatasetKey(key) { 164 if (!isNonEmptyString(key) || !VALID_KEY.test(key)) { 165 return null; 166 } 167
168 const parts = key.split(/[-_]/).filter(Boolean); 169 if (!parts.length) { 170 return null; 171 } 172 173 const pascal = parts 174 .map(function (part) { 175 return part.charAt(0).toUpperCase() + part.slice(1); 176 }) 177 .join(""); 178 179 return DATASET_PREFIX + pascal; 180 } 181 182 function bindOnce(key, fn) { 183 if (!isNonEmptyString(key) || typeof fn !== "function") { 184 return; 185 } 186 if (onceKeys[key]) { 187 return; 188 } 189 onceKeys[key] = true; 190 fn(); 191 } 192 193 function isInitialized(el, key) { 194 if (!hasDataset(el)) { 195 return false; 196 } 197 const datasetKey = toDatasetKey(key); 198 if (!datasetKey) { 199 return false; 200 } 201 return !!el.dataset[datasetKey]; 202 } 203 204 function markInitialized(el, key) { 205 if (!hasDataset(el)) { 206 return; 207 } 208 const datasetKey = toDatasetKey(key); 209 if (!datasetKey) { 210 return; 211 } 212 el.dataset[datasetKey] = "true"; 213 } 214 215 function initScope(root, fns) { 216 const scope = root || document; 217 if (!Array.isArray(fns)) { 218 return; 219 } 220 fns.forEach(function (fn) { 221 if (typeof fn === "function") { 222 fn(scope); 223 } 224 }); 225 } 226 227 /** 228 * Register a single body listener via bindOnce. 229 * options: { targetId?, onceKey? } 230 * When targetId is set, handler runs only if event.detail.target.id matches. 231 */ 232 function onBody(eventName, handler, options) { 233 if (!isNonEmptyString(eventName) || typeof handler !== "function") { 234 return; 235 } 236 237 const opts = options || {}; 238 const targetId = opts.targetId; 239 const onceKey = 240 opts.onceKey || eventName + ":" + (targetId || "*"); 241 242 bindOnce(onceKey, function () { 243 document.body.addEventListener(eventName, function (event) { 244 if (targetId) { 245 const eventTarget = event.detail && event.detail.target; 246 if (!eventTarget || eventTarget.id !== targetId) { 247 return; 248 } 249 } 250 handler(event); 251 }); 252 }); 253 } 254 255 return { 256 bindOnce: bindOnce, 257 isInitialized: isInitialized, 258 markInitialized: markInitialized, 259 initScope: initScope, 260 onBody: onBody, 261 }; 262})(); 263; 264kombi.cleaveMask = function () { 265 document.querySelectorAll("input.cleave").forEach((item) => { 266 let opts = { 267 creditCard: this.getBoolAttr(item, "data-credit-card", false), 268 phone: this.getBoolAttr(item, "data-phone", false), 269 phoneRegionCode: item.getAttribute("data-phone-region-code") || "br", 270 date: this.getBoolAttr(item, "data-date", false), 271 datePattern: (item.getAttribute("data-date-pattern") || "d/m/Y").split( 272 "/" 273 ), 274 dateMin: item.getAttribute("data-date-min"), 275 dateMax: item.getAttribute("data-date-max"), 276 time: this.getBoolAttr(item, "data-time", false), 277 timePattern: (item.getAttribute("data-time-pattern") || "h:m").split(":"), 278 timeFormat: item.getAttribute("data-time-format") || "24", 279 blocks: item 280 .getAttribute("data-blocks") 281 ?.split("|") 282 .map((x) => parseInt(x)), 283 delimiter: item.getAttribute("data-delimiters"), 284 delimiters: item.getAttribute("data-delimiters")?.split("|"), 285 uppercase: this.getBoolAttr(item, "data-uppercase", false), 286 lowercase: this.getBoolAttr(item, "data-lowercase", false), 287 numeral: this.getBoolAttr(item, "data-numeral", false), 288 numericOnly: this.getBoolAttr(item, "data-numeric-only", false), 289 numeralThousandsGroupStyle: item.getAttribute( 290 "data-numeral-thousands-group-style" 291 ), 292 numeralDecimalMark: item.getAttribute("data-numeral-decimal-mark") || ",", 293 prefix: item.getAttribute("data-prefix"), 294 tailPrefix: this.getBoolAttr(item, "data-tail-prefix", false), 295 }; 296 new Cleave(item, opts); 297 }); 298}; 299; 300kombi.carousel = function (root) { 301 const scope = root && root.querySelector ? root : document; 302 const carousel = 303 scope.id === "hotelCarousel" 304 ? scope 305 : scope.querySelector("#hotelCarousel"); 306 307 if (!carousel) { 308 return; 309 } 310 if (kombi.lifecycle.isInitialized(carousel, "hotel-carousel")) { 311 return; 312 } 313 kombi.lifecycle.markInitialized(carousel, "hotel-carousel"); 314 315 function deferHotelCarouselAutoplay(carouselEl) { 316 if (!carouselEl || !window.bootstrap || !bootstrap.Carousel) { 317 return; 318 } 319 if (carouselEl.querySelectorAll(".carousel-item").length < 2) { 320 return; 321 } 322 323 function startCycle() {
324 bootstrap.Carousel.getOrCreateInstance(carouselEl).cycle(); 325 } 326 327 function onIdle(callback) { 328 if (typeof window.requestIdleCallback === "function") { 329 window.requestIdleCallback(callback, { timeout: 4000 }); 330 } else { 331 window.setTimeout(callback, 1); 332 } 333 } 334 335 if (document.readyState === "complete") { 336 onIdle(startCycle); 337 } else { 338 window.addEventListener( 339 "load", 340 function () { 341 onIdle(startCycle); 342 }, 343 { once: true } 344 ); 345 } 346 } 347 348 function parseSlideIndex(value, fallback) { 349 const parsed = Number(value); 350 return Number.isInteger(parsed) ? parsed : fallback; 351 } 352 353 deferHotelCarouselAutoplay(carousel); 354 355 const thumbTrack = carousel.querySelector(".carousel-indicators"); 356 if (!thumbTrack) { 357 return; 358 } 359 360 const indicators = Array.from(thumbTrack.querySelectorAll("img[data-bs-slide-to]")); 361 const geometry = { 362 sliderWidth: 0, 363 maxScroll: 0, 364 thumbCenters: [], 365 }; 366 const RELEASE_EVENTS = ["pointerup", "pointercancel"]; 367 368 function readGeometry() { 369 geometry.sliderWidth = thumbTrack.clientWidth; 370 geometry.maxScroll = Math.max(0, thumbTrack.scrollWidth - thumbTrack.clientWidth); 371 geometry.thumbCenters = indicators.map(function (thumb) { 372 return thumb.offsetLeft + thumb.offsetWidth / 2; 373 }); 374 } 375 376 function scrollThumbIntoView(index) { 377 const center = geometry.thumbCenters[index]; 378 if (center == null) { 379 return; 380 } 381 thumbTrack.scrollLeft = Math.min( 382 geometry.maxScroll, 383 Math.max(0, center - geometry.sliderWidth / 2) 384 ); 385 } 386 387 function setActiveThumb(fromIndex, toIndex) { 388 const prev = indicators[fromIndex]; 389 const next = indicators[toIndex]; 390 if (prev && prev !== next) { 391 prev.classList.remove("active"); 392 prev.removeAttribute("aria-current"); 393 } 394 if (next) { 395 next.classList.add("active"); 396 next.setAttribute("aria-current", "true"); 397 } 398 scrollThumbIntoView(toIndex); 399 } 400 401 function bindTrackDrag() { 402 let isPointerDown = false; 403 let isDragging = false; 404 let startX = 0; 405 let startScroll = 0; 406 let pendingX = 0; 407 let scrollFrameId = 0; 408 let suppressClick = false; 409 const DRAG_SPEED = 3; 410 const DRAG_THRESHOLD = 8; 411 412 function applyScroll() { 413 scrollFrameId = 0; 414 if (!isDragging) { 415 return; 416 } 417 thumbTrack.scrollLeft = startScroll - (pendingX - startX) * DRAG_SPEED; 418 } 419 420 function setDocumentRelease(enabled) { 421 const method = enabled ? "addEventListener" : "removeEventListener"; 422 RELEASE_EVENTS.forEach(function (name) { 423 document[method](name, endDrag); 424 }); 425 } 426 427 function onPointerDown(event) { 428 if (event.pointerType !== "mouse" || event.button !== 0) { 429 return; 430 } 431 isPointerDown = true; 432 isDragging = false; 433 suppressClick = false; 434 startX = event.clientX; 435 pendingX = startX; 436 startScroll = thumbTrack.scrollLeft; 437 setDocumentRelease(true); 438 } 439 440 function onPointerMove(event) { 441 if (!isPointerDown) { 442 return; 443 } 444 if (event.buttons === 0) { 445 endDrag(event); 446 return; 447 } 448 pendingX = event.clientX; 449 if (!isDragging) { 450 if (Math.abs(pendingX - startX) <= DRAG_THRESHOLD) { 451 return; 452 } 453 isDragging = true; 454 if (thumbTrack.setPointerCapture) { 455 thumbTrack.setPointerCapture(event.pointerId); 456 } 457 } 458 event.preventDefault(); 459 if (!scrollFrameId) { 460 scrollFrameId = window.requestAnimationFrame(applyScroll); 461 } 462 } 463 464 function endDrag(event) { 465 if (!isPointerDown) { 466 return; 467 } 468 isPointerDown = false;
469 setDocumentRelease(false); 470 if (isDragging) { 471 suppressClick = true; 472 if (scrollFrameId) { 473 window.cancelAnimationFrame(scrollFrameId); 474 applyScroll(); 475 } 476 if ( 477 event && 478 thumbTrack.releasePointerCapture && 479 thumbTrack.hasPointerCapture && 480 thumbTrack.hasPointerCapture(event.pointerId) 481 ) { 482 thumbTrack.releasePointerCapture(event.pointerId); 483 } 484 } 485 isDragging = false; 486 } 487 488 thumbTrack.addEventListener("pointerdown", onPointerDown); 489 thumbTrack.addEventListener("pointermove", onPointerMove); 490 thumbTrack.addEventListener( 491 "click", 492 function (event) { 493 if (!suppressClick) { 494 return; 495 } 496 suppressClick = false; 497 event.preventDefault(); 498 event.stopPropagation(); 499 }, 500 true 501 ); 502 } 503 504 if (typeof ResizeObserver === "function") { 505 new ResizeObserver(readGeometry).observe(thumbTrack); 506 } 507 508 bindTrackDrag(); 509 readGeometry(); 510 511 carousel.addEventListener("slid.bs.carousel", function (event) { 512 const nextIndex = parseSlideIndex(event.to, 0); 513 const prevIndex = parseSlideIndex(event.from, 0); 514 if (!geometry.thumbCenters.length) { 515 readGeometry(); 516 } 517 setActiveThumb(prevIndex, nextIndex); 518 }); 519}; 520; 521// Shared calendar day pricing (desktop + mobile). GET calendar.json?hotel_pk=&mode=hotel|package. 522kombi.calendarDayPrice = (function () { 523 const MISSING_PRICE_LABEL = "---"; 524 525 let lastHotelPk = null; 526 let lastCalendarUrl = null; 527 let lastData = null; 528 let fetchSeq = 0; 529 530 function normalizeCalendarData(data) { 531 if (!data || typeof data !== "object") { 532 return null; 533 } 534 // Feature flag off / no rates â {} ; treat as no calendar for UI. 535 if (Object.keys(data).length === 0) { 536 return null; 537 } 538 return data; 539 } 540 541 function isPackageActive() { 542 return ( 543 kombi.searchTabs && 544 typeof kombi.searchTabs.isPackageActive === "function" && 545 kombi.searchTabs.isPackageActive() 546 ); 547 } 548 549 function getMode() { 550 return isPackageActive() ? "package" : "hotel"; 551 } 552 553 function toDateKey(dateObj) { 554 if (typeof kombi.fp_date_key === "function") { 555 return kombi.fp_date_key(dateObj); 556 } 557 return ""; 558 } 559 560 function formatPrice(value) { 561 const amount = Number(value); 562 if (!Number.isFinite(amount) || amount <= 0) { 563 return ""; 564 } 565 return new Intl.NumberFormat("pt-BR", { 566 minimumFractionDigits: 0, 567 maximumFractionDigits: 0, 568 }).format(Math.round(amount)); 569 } 570 571 function buildUrl(calendarUrl, hotelPk, mode, refresh) { 572 const url = new URL(calendarUrl, window.location.origin); 573 url.searchParams.set("hotel_pk", String(hotelPk)); 574 url.searchParams.set("mode", mode || getMode()); 575 if (refresh) { 576 url.searchParams.set("refresh", "1"); 577 } 578 return url.toString(); 579 } 580 581 function ensurePriceSlot(dayElement) { 582 let priceEl = dayElement.querySelector(".calendar-day-price"); 583 if (!priceEl) { 584 priceEl = document.createElement("span"); 585 priceEl.className = "calendar-day-price"; 586 priceEl.setAttribute("aria-hidden", "true"); 587 dayElement.appendChild(priceEl); 588 } 589 return priceEl; 590 } 591 592 function removePriceSlot(dayElement) { 593 const priceEl = dayElement.querySelector(".calendar-day-price"); 594 if (priceEl) { 595 priceEl.remove(); 596 } 597 } 598 599 // Package: no monetary value; best-price teal on the day number only. 600 function decoratePackageDay(dayElement, calendarDay, canArrive) { 601 removePriceSlot(dayElement); 602 if (canArrive && calendarDay.is_best_price === true) { 603 dayElement.classList.add("calendar-best-price"); 604 } 605 } 606 607 // Hotel: daily rate under the day number;
607 keep the slot for cell alignment. 608 function decorateHotelDay(dayElement, calendarDay, canArrive) { 609 const priceEl = ensurePriceSlot(dayElement); 610 priceEl.textContent = ""; 611 priceEl.classList.remove("calendar-day-price--best"); 612 613 if (!canArrive) { 614 return; 615 } 616 617 if (calendarDay.is_best_price === true) { 618 dayElement.classList.add("calendar-best-price"); 619 priceEl.classList.add("calendar-day-price--best"); 620 } 621 622 priceEl.textContent = formatPrice(calendarDay.daily_price) || MISSING_PRICE_LABEL; 623 } 624 625 function decorateDayElement(dayElement, calendarDay, canArrive) { 626 if (!dayElement) { 627 return; 628 } 629 630 dayElement.classList.remove("calendar-best-price"); 631 632 if (isPackageActive()) { 633 decoratePackageDay(dayElement, calendarDay, canArrive); 634 return; 635 } 636 637 decorateHotelDay(dayElement, calendarDay, canArrive); 638 } 639 640 function applyToInstances(data) { 641 (kombi.flatpickrInstances || []).forEach(function (fp) { 642 fp.calendar = data; 643 fp.redraw(); 644 }); 645 if (typeof kombi.syncAllCalendarLegendVisibility === "function") { 646 kombi.syncAllCalendarLegendVisibility(); 647 } 648 if ( 649 kombi.calendarMobile && 650 typeof kombi.calendarMobile.applyCalendarData === "function" 651 ) { 652 kombi.calendarMobile.applyCalendarData(data); 653 } 654 } 655 656 function fetchCalendar(calendarUrl, hotelPk, mode, refresh) { 657 const resolvedMode = mode || getMode(); 658 if (!hotelPk || !calendarUrl) { 659 fetchSeq += 1; 660 lastHotelPk = null; 661 lastCalendarUrl = null; 662 lastData = null; 663 applyToInstances(null); 664 return Promise.resolve(null); 665 } 666 667 const requestSeq = (fetchSeq += 1); 668 const requestUrl = buildUrl(calendarUrl, hotelPk, resolvedMode, refresh); 669 return fetch(requestUrl) 670 .then(function (response) { 671 if (!response.ok) { 672 throw new Error("Calendar request failed: " + response.status); 673 } 674 return response.json(); 675 }) 676 .then(function (data) { 677 if (requestSeq !== fetchSeq) { 678 return lastData; 679 } 680 const normalized = normalizeCalendarData(data); 681 lastHotelPk = hotelPk; 682 lastCalendarUrl = calendarUrl; 683 lastData = normalized; 684 applyToInstances(normalized); 685 return normalized; 686 }) 687 .catch(function () { 688 if (requestSeq !== fetchSeq) { 689 return lastData; 690 } 691 lastData = null; 692 applyToInstances(null); 693 return null; 694 }); 695 } 696 697 function refetchForCurrentMode() { 698 if (!lastHotelPk || !lastCalendarUrl) { 699 return Promise.resolve(null); 700 } 701 return fetchCalendar(lastCalendarUrl, lastHotelPk, getMode()); 702 } 703 704 // Calendar open only: send refresh=1 so the backend may enqueue maybe_refresh. 705 function refreshOnCalendarOpen() { 706 if (!lastHotelPk || !lastCalendarUrl) { 707 return Promise.resolve(null); 708 } 709 return fetchCalendar(lastCalendarUrl, lastHotelPk, getMode(), true); 710 } 711 712 return { 713 toDateKey: toDateKey, 714 decorateDayElement: decorateDayElement, 715 fetchCalendar: fetchCalendar, 716 refetchForCurrentMode: refetchForCurrentMode, 717 refreshOnCalendarOpen: refreshOnCalendarOpen, 718 getCachedData: function () { 719 return lastData; 720 }, 721 getLastHotelPk: function () { 722 return lastHotelPk; 723 }, 724 isPackageActive: isPackageActive, 725 }; 726})(); 727; 728kombi.flatpickrInstances = []; 729 730/** Local YYYY-MM-DD (avoids toISOString timezone cost/skew in hot calendar paths). */ 731kombi.fp_date_key = function (dateObj) { 732 if (!(dateObj instanceof Date) || isNaN(dateObj.getTime())) { 733 return ""; 734 } 735 const year = dateObj.getFullYear();
736 const month = String(dateObj.getMonth() + 1).padStart(2, "0"); 737 const day = String(dateObj.getDate()).padStart(2, "0"); 738 return year + "-" + month + "-" + day; 739}; 740 741kombi.fp_can_stay = function (start, end, calendar) { 742 let currentDate = new Date(start); 743 let dateStr = kombi.fp_date_key(currentDate); 744 let duration = (end - start) / 86400000; 745 if (start == end || duration < calendar[dateStr].min_stay || duration > calendar[dateStr].max_stay) { 746 return false; 747 } 748 while (currentDate < end) { 749 if (!calendar[dateStr] || calendar[dateStr].available <= 0) { 750 return false; 751 } 752 currentDate.setDate(currentDate.getDate() + 1); 753 dateStr = kombi.fp_date_key(currentDate); 754 } 755 return true; 756} 757 758kombi.fp_can_start_stay = function (start, calendar) { 759 let currentStayStart = new Date(start); 760 let currentDate = new Date(); 761 currentDate.setHours(0, 0, 0, 0); 762 let dateStr = kombi.fp_date_key(currentStayStart); 763 let antecedence = calendar[dateStr].antecedence; 764 if (currentStayStart < currentDate.fp_incr(antecedence)) { 765 return false; 766 } 767 // check that all days between start and min_stay are available 768 let min_stay = calendar[dateStr].min_stay; 769 let end = currentStayStart.fp_incr(min_stay); 770 while (currentStayStart < end) { 771 if (!calendar[dateStr] || calendar[dateStr].available <= 0) { 772 return false; 773 } 774 currentStayStart.setDate(currentStayStart.getDate() + 1); 775 dateStr = kombi.fp_date_key(currentStayStart); 776 } 777 return true; 778} 779 780/** Date accepts a check-in: has inventory, arrival is open and antecedence/min-stay fit. */ 781kombi.fp_can_arrive = function (dateObj, calendar) { 782 const calendarDay = calendar ? calendar[kombi.fp_date_key(dateObj)] : null; 783 if (!calendarDay) { 784 return false; 785 } 786 return ( 787 calendarDay.available > 0 && 788 !calendarDay.closed_on_arrival && 789 kombi.fp_can_start_stay(dateObj, calendar) 790 ); 791}; 792 793/** Day availability + pricing shared by the desktop popup and the mobile modal (onDayCreate). */ 794kombi.fp_apply_day_rules = function (fp, dayElem) {
795 if (!fp.calendar || Object.keys(fp.calendar).length === 0) { 796 return; 797 } 798 799 const calendarDay = fp.calendar[kombi.fp_date_key(dayElem.dateObj)] || null; 800 const canArrive = kombi.fp_can_arrive(dayElem.dateObj, fp.calendar); 801 let showMinStay = false; 802 803 if (kombi.calendarDayPrice) { 804 kombi.calendarDayPrice.decorateDayElement(dayElem, calendarDay, canArrive); 805 } 806 807 if (!calendarDay) { 808 if ( 809 !(fp.selecting_stay && kombi.fp_can_stay(fp.selecting_stay, dayElem.dateObj, fp.calendar)) 810 ) { 811 dayElem.classList.add("flatpickr-disabled"); 812 } 813 return; 814 } 815 816 if (!fp.selecting_stay) { 817 if (canArrive) { 818 showMinStay = true; 819 } else { 820 dayElem.classList.add("flatpickr-disabled"); 821 } 822 } else if (dayElem.dateObj.getTime() === fp.selecting_stay.getTime()) { 823 dayElem.classList.add("flatpickr-disabled", "selecting"); 824 showMinStay = true; 825 } else if ( 826 !( 827 dayElem.dateObj >= fp.selecting_stay && 828 kombi.fp_can_stay(fp.selecting_stay, dayElem.dateObj, fp.calendar) && 829 !calendarDay.closed_on_departure 830 ) 831 ) { 832 dayElem.classList.add("flatpickr-disabled"); 833 } 834 835 // Touch has no hover: the tip must exist in the DOM, CSS reveals it on .selecting. 836 if (showMinStay) { 837 const tooltip = document.createElement("span"); 838 tooltip.classList.add("calendar-tooltip"); 839 tooltip.textContent = "MÃn de " + calendarDay.min_stay + " noite(s)"; 840 dayElem.appendChild(tooltip); 841 } 842}; 843 844/** 845 * Show Melhores preços / Entrada indisponÃvel legends when hotel calendar API data exists 846 * (Hotel or Package with hotel). Rates note ("Tarifas estimadas...") only on Hotel tab. 847 * Empty payload ({}) / cleared hotel â hide. 848 */ 849kombi.setCalendarLegendVisibility = function (fp) { 850 const container = fp.calendarContainer; 851 if (!container) return; 852 const hasCalendarData = !!(fp.calendar && Object.keys(fp.calendar).length > 0); 853 const isPackage = 854 kombi.calendarDayPrice && 855 typeof kombi.calendarDayPrice.isPackageActive === "function" && 856 kombi.calendarDayPrice.isPackageActive(); 857 const showRatesNote = hasCalendarData && !isPackage; 858 const legendContainer = container.querySelector(".calendar-legends"); 859 if (legendContainer) { 860 legendContainer.classList.toggle("d-none", !hasCalendarData); 861 legendContainer.classList.toggle("d-flex", hasCalendarData); 862 } 863 const ratesNote = container.querySelector(".calendar-rates-note"); 864 if (ratesNote) { 865 ratesNote.classList.toggle("d-none", !showRatesNote); 866 ratesNote.setAttribute("aria-hidden", showRatesNote ? "false" : "true"); 867 } 868}; 869 870kombi.syncAllCalendarLegendVisibility = function () { 871 (kombi.flatpickrInstances || []).forEach(function (fp) { 872 kombi.setCalendarLegendVisibility(fp); 873 }); 874}; 875 876kombi.flatpickr = function () { 877 /** 878 * Package (search): lead time is read from <body data-package-lead-days="N"> (set by backend). 879 * First allowed check-in = today + N + 1 (exclusive: today+N is not selectable). 880 * Hotel: keeps new Date().fp_incr(1). 881 */ 882 const DEFAULT_PACKAGE_LEAD_DAYS = 5; 883 884 function getPackageLeadDays() { 885 var raw = document.body && document.body.dataset 886 ? document.body.dataset.packageLeadDays 887 : null; 888 var parsed = parseInt(raw, 10); 889 return (parsed === parsed && parsed >= 0) ? parsed : DEFAULT_PACKAGE_LEAD_DAYS; 890 } 891 892 function getPackageMinDate() { 893 const d = new Date(); 894 d.setHours(0, 0, 0, 0); 895 d.setDate(d.getDate() + getPackageLeadDays() + 1); 896 return d; 897 } 898 899 function isPackageActive() { 900 return !!( 901 kombi.searchTabs && 902 typeof kombi.searchTabs.isPackageActive === "function" && 903 kombi.searchTabs.isPackageActive() 904 ); 905 } 906 907 function getMinDateForCurrentTab() { 908 if (isPackageActive()) { 909 return getPackageMinDate(); 910 } 911 return new Date().fp_incr(1); 912 } 913 914 /** If the visible month is before minDate's month, jump the view forward. */ 915 function jumpToPackageMinMonthIfNeeded(fp, minDate) { 916 const viewMonthStart = new Date(fp.currentYear, fp.currentMonth, 1); 917 const minMonthStart = new Date(minDate.getFullYear(), minDate.getMonth(), 1); 918 if (viewMonthStart < minMonthStart) { 919 fp.jumpToDate(minDate); 920 } 921 } 922 923 /** Legends match product mocks for Hotel and Package: Melhores preços + Entrada indisponÃvel. */ 924 function getCalendarLegendsInnerHTML() { 925 const firstLegend = 926 '<div class="legend d-flex align-items-center mt-1 w-auto">' + 927 '<div class="legend-color best-prices rounded-circle me-3" style="width: 15px; height: 15px; flex-shrink: 0;"></div><span class="text-start lh-1 fs-0_8rem">Melhores preços</span>' + 928 "</div>"; 929 const secondLegend = 930 '<div class="legend d-flex align-items-center mt-1 w-auto">' + 931 '<div class="position-relative me-3" style="width: 15px; height: 15px; flex-shrink: 0;">' + 932 '<div class="legend-color date-unavailable rounded-circle" style="width: 15px; height: 15px;"></div>' + 933 '<div class="border-top border-2 position-absolute top-50 translate-middle-y" style="left: -3.5px; right: -3px; border-color: #626262 !important;"></div>' + 934 "</div>" + 935 '<span class="text-start lh-1 fs-0_8rem">Entrada indisponÃvel</span>' + 936 "</div>"; 937 return firstLegend + secondLegend; 938 } 939 940 function updateAllCalendarLegendsForTab() { 941 const html = getCalendarLegendsInnerHTML();
942 kombi.flatpickrInstances.forEach(function (fp) { 943 const legendContainer = fp.calendarContainer && fp.calendarContainer.querySelector(".calendar-legends"); 944 if (legendContainer) { 945 legendContainer.innerHTML = html; 946 } 947 kombi.setCalendarLegendVisibility(fp); 948 }); 949 } 950 951 function syncClearButtonState(fp) { 952 if (!fp || !fp.calendarContainer) return; 953 const clearBtn = fp.calendarContainer.querySelector(".calendar-btn-clear"); 954 if (!clearBtn) return; 955 const selected = fp.selectedDates || []; 956 clearBtn.disabled = selected.length === 0; 957 } 958 959 /** Desktop: center dual-month popup under .nav-search (measured, not CSS tokens). */ 960 function centerCalendarUnderSearchBar(fp) { 961 const cal = fp && fp.calendarContainer; 962 if ( 963 !cal || 964 cal.classList.contains("calendar-mobile__fp") || 965 !window.matchMedia("(min-width: 992px)").matches 966 ) { 967 return; 968 } 969 const bar =
970 (fp.element && fp.element.closest(".nav-search")) || 971 document.querySelector(".d-sm-block .nav-search"); 972 if (!bar || !cal.offsetWidth) return; 973 974 const barRect = bar.getBoundingClientRect(); 975 cal.style.left = 976 barRect.left + (barRect.width - cal.offsetWidth) / 2 + window.pageXOffset + "px"; 977 cal.style.right = "auto"; 978 cal.style.transform = "none"; 979 cal.classList.remove("rightMost", "centerMost"); 980 } 981 982 function clearCalendarDates(fp, rootItem) { 983 if (!fp || !rootItem) return; 984 const currentMonth = fp.currentMonth; 985 const currentYear = fp.currentYear; 986 fp.selecting_stay = null; 987 fp.clear(); 988 fp.jumpToDate(new Date(currentYear, currentMonth)); 989 const startInput = rootItem.querySelector(".dates-0"); 990 const endInput = rootItem.querySelector(".dates-1"); 991 if (startInput) startInput.value = ""; 992 if (endInput) endInput.value = ""; 993 syncClearButtonState(fp); 994 } 995 996 /** 997 * Flatpickr weekday headers: single letter (D S T Q Q S S), matching the search calendar mock. 998 */ 999 function getCalendarLocale(localeCode) { 1000 const code = localeCode || "pt"; 1001 const base = 1002 typeof flatpickr !== "undefined" && 1003 flatpickr.l10ns && 1004 flatpickr.l10ns[code] 1005 ? flatpickr.l10ns[code] 1006 : null; 1007 if (!base) return code; 1008 return Object.assign({}, base, { 1009 weekdays: Object.assign({}, base.weekdays, {
1010 shorthand: ["D", "S", "T", "Q", "Q", "S", "S"], 1011 }), 1012 }); 1013 } 1014 1015 // Search desktop (#search-calendar-desk). Mobile search uses calendarMobile (inline modal). 1016 // https://flatpickr.js.org/ 1017 document.querySelectorAll(".flatpickr").forEach((item) => { 1018 let enableTime = this.getBoolAttr(item, "data-enable-time", false); 1019 let initialHotel = item.getAttribute("data-initial-hotel") || null; 1020 let opts = { 1021 mode: item.getAttribute("data-mode") || "single", 1022 enableTime: enableTime, 1023 noCalendar: this.getBoolAttr(item, "data-no-calendar", false), 1024 inline: this.getBoolAttr(item, "data-inline", false), 1025 animate: this.getBoolAttr(item, "data-animate", false), 1026 position: "auto", 1027 dateFormat: 1028 item.getAttribute("data-date-format") || (enableTime ? "H:i" : "Y-m-d"), 1029 disableMobile: this.getBoolAttr(item, "data-disable-mobile", true), 1030 defaultDate: item.getAttribute("data-default-date")?.split(",") || null, 1031 altInput: this.getBoolAttr(item, "data-alt-input", true), 1032 altFormat: 1033 item.getAttribute("data-alt-format") || (enableTime ? "H:i" : "d/m/y"), 1034 locale: getCalendarLocale(item.getAttribute("data-locale") || "pt"), 1035 minDate: item.getAttribute("data-min-date") || getMinDateForCurrentTab(), 1036 maxDate: item.getAttribute("data-max-date") || new Date().fp_incr(540), 1037 wrap: this.getBoolAttr(item, "data-wrap", false), 1038 showMonths: 2, 1039 // Keep open after range select; user confirms with footer Avançar. 1040 closeOnSelect: false, 1041 // Month label is text-only; avoid dropdown jumps. Year input is locked in onReady/CSS. 1042 monthSelectorType: "static", 1043 onOpen: function(selectedDates, dateStr, instance) {
1044 const searchControlsRoot = 1045 item.closest(".nav-search") || 1046 item.closest("#SearchForm .pattern-form"); 1047 if (searchControlsRoot && kombi.closeNavSearchDropdowns) { 1048 kombi.closeNavSearchDropdowns(searchControlsRoot); 1049 } 1050 if (this.config.mode == "range") { 1051 if (instance.selectedDates.length === 1) { 1052 instance.selectedDates = []; 1053 } 1054 instance.selecting_stay = null; 1055 instance.redraw(); 1056 } 1057 syncClearButtonState(instance); 1058 if (isPackageActive() && instance.config.mode === "range") { 1059 jumpToPackageMinMonthIfNeeded(instance, getPackageMinDate()); 1060 } 1061 // Width may settle after Flatpickr's first position pass. 1062 requestAnimationFrame(function () { 1063 centerCalendarUnderSearchBar(instance); 1064 }); 1065 if (kombi.calendarDayPrice) { 1066 kombi.calendarDayPrice.refreshOnCalendarOpen(); 1067 } 1068 }, 1069 onClose: function (selectedDates) { 1070 if (this.config.mode === "range") { 1071 const startInput = item.querySelector(".dates-0"); 1072 const endInput = item.querySelector(".dates-1"); 1073 const incompleteRange = 1074 selectedDates.length === 1 || 1075 (selectedDates.length === 2 && 1076 selectedDates[0].getTime() >= selectedDates[1].getTime()); 1077 1078 // Incomplete selection must not persist a lone check-in into hidden inputs. 1079 if (incompleteRange) { 1080 this.selecting_stay = null; 1081 this.clear(); 1082 if (startInput) startInput.value = ""; 1083 if (endInput) endInput.value = ""; 1084 return; 1085 } 1086 1087 this.selecting_stay = null; 1088 if (selectedDates[0] && startInput) { 1089 startInput.value = this.formatDate(selectedDates[0], "Y-m-d"); 1090 } 1091 if (selectedDates[1] && endInput) { 1092 endInput.value = this.formatDate(selectedDates[1], "Y-m-d"); 1093 } 1094 } 1095 }, 1096 onChange: function (selectedDates, dateStr, instance) { 1097 if (instance.config.mode == "range" && selectedDates.length === 1) { 1098 instance.selecting_stay = selectedDates[0]; 1099 instance.redraw(); 1100 } else { 1101 instance.selecting_stay = null; 1102 } 1103 syncClearButtonState(instance); 1104 }, 1105 onDayCreate: function (dObj, dStr, fp, dayElem) { 1106 kombi.fp_apply_day_rules(fp, dayElem); 1107 }, 1108 onReady: function(selectedDates, dateStr, instance) { 1109 if (!instance.calendarContainer) return; 1110 1111 const legendsHtml = getCalendarLegendsInnerHTML(); 1112 1113 const footerWrap = document.createElement("div"); 1114 footerWrap.classList.add("calendar-footer-wrap", "mx-2", "mt-1"); 1115 1116 const ratesNote = document.createElement("div"); 1117 ratesNote.classList.add( 1118 "calendar-rates-note", 1119 "d-none", 1120 "pb-2", 1121 "fs-0_8rem", 1122 "text-gray-650", 1123 "text-end", 1124 ); 1125 ratesNote.textContent = "Tarifas estimadas para 02 adultos"; 1126 ratesNote.setAttribute("aria-hidden", "true"); 1127 1128 const footer = document.createElement("div"); 1129 footer.classList.add( 1130 "calendar-footer", 1131 "d-flex", 1132 "align-items-center", 1133 "justify-content-between", 1134 "gap-3", 1135 "pt-2", 1136 "border-top", 1137 ); 1138 1139 const legendContainer = document.createElement("div"); 1140 legendContainer.classList.add( 1141 "calendar-legends", 1142 "d-flex", 1143 "align-items-center", 1144 "gap-4", 1145 "flex-wrap" 1146 ); 1147 legendContainer.innerHTML = legendsHtml; 1148 1149 const actions = document.createElement("div"); 1150 actions.classList.add( 1151 "calendar-actions", 1152 "d-flex", 1153 "align-items-center", 1154 "gap-2", 1155 "flex-shrink-0", 1156 "ms-auto" 1157 ); 1158 actions.innerHTML = 1159 '<button type="button" class="btn btn-sm btn-outline-primary rounded-pill calendar-btn-clear px-3 py-2" disabled>Limpar</button>' + 1160 '<button type="button" class="btn btn-sm btn-primary rounded-pill calendar-btn-confirm px-3 py-2">Avançar</button>'; 1161 1162 footer.appendChild(legendContainer); 1163 footer.appendChild(actions); 1164 footerWrap.appendChild(ratesNote); 1165 footerWrap.appendChild(footer); 1166 instance.calendarContainer.appendChild(footerWrap); 1167 1168 actions.querySelector(".calendar-btn-clear").addEventListener("click", function (e) { 1169 e.preventDefault(); 1170 e.stopPropagation(); 1171 if (e.currentTarget.disabled) return; 1172 clearCalendarDates(instance, item); 1173 }); 1174 actions.querySelector(".calendar-btn-confirm").addEventListener("click", function (e) { 1175 e.preventDefault(); 1176 e.stopPropagation(); 1177 const dates = instance.selectedDates || []; 1178 if ( 1179 dates.length === 2 && 1180 dates[0].getTime() < dates[1].getTime() 1181 ) { 1182 instance.close(); 1183 } 1184 }); 1185 syncClearButtonState(instance); 1186 kombi.setCalendarLegendVisibility(instance); 1187 }, 1188 }; 1189 1190 let fpInstance = flatpickr(item, opts); 1191 fpInstance.calendar = null; 1192 fpInstance.selecting_stay = null; 1193 fpInstance.calendar_url = item.getAttribute("data-calendar-url") || null; 1194 fpInstance.initialHotel = initialHotel; 1195 // After every Flatpickr position (open/resize), fix horizontal align to .nav-search. 1196 const positionCalendar = fpInstance._positionCalendar.bind(fpInstance); 1197 fpInstance._positionCalendar = function () {
1198 positionCalendar(); 1199 centerCalendarUnderSearchBar(fpInstance); 1200 }; 1201 kombi.flatpickrInstances.push(fpInstance); 1202 }); 1203 1204 if (this.flatpickrInstances.length > 0) { 1205 if (this.flatpickrInstances[0].initialHotel && this.flatpickrInstances[0].calendar_url) { 1206 this.updateCalendar(this.flatpickrInstances[0].initialHotel); 1207 } 1208 } 1209 1210 if (!kombi._searchTabChangeFlatpickrBound) { 1211 kombi._searchTabChangeFlatpickrBound = true; 1212 document.addEventListener("searchtab:change", function (e) { 1213 const isPackage = e.detail && e.detail.isPackage; 1214 const minDate = isPackage ? getPackageMinDate() : new Date().fp_incr(1); 1215 kombi.flatpickrInstances.forEach(function (fp) { 1216 fp.set("minDate", minDate); 1217 const dates = fp.selectedDates || []; 1218 const minTs = minDate.getTime(); 1219 const hasInvalid = dates.some(function (d) { 1220 return d.getTime() < minTs; 1221 }); 1222 if (hasInvalid) { 1223 fp.clear(); 1224 const el = fp.element; 1225 const d0 = el.querySelector(".dates-0"); 1226 const d1 = el.querySelector(".dates-1"); 1227 if (d0) d0.value = ""; 1228 if (d1) d1.value = ""; 1229 } 1230 if (isPackage) { 1231 jumpToPackageMinMonthIfNeeded(fp, minDate); 1232 } 1233 fp.redraw(); 1234 }); 1235 updateAllCalendarLegendsForTab(); 1236 if (kombi.calendarDayPrice) { 1237 kombi.calendarDayPrice.refetchForCurrentMode(); 1238 } 1239 kombi.flatpickrInstances.forEach(function (fp) { 1240 if (fp.isOpen) centerCalendarUnderSearchBar(fp); 1241 }); 1242 }); 1243 } 1244}; 1245 1246kombi.updateCalendar = function (hotel_pk) { 1247 // Keep mobile resolveHotelPk in sync with search-bar hotel selection/clear. 1248 // Otherwise detail-page data-initial-hotel (e.g. Blue Tree) sticks after a switch/clear. 1249 document 1250 .querySelectorAll("#calendar-mobile-mount, .calendar-mobile-trigger") 1251 .forEach(function (el) { 1252 if (hotel_pk) { 1253 el.setAttribute("data-initial-hotel", String(hotel_pk)); 1254 } else { 1255 el.removeAttribute("data-initial-hotel"); 1256 } 1257 }); 1258 (this.flatpickrInstances || []).forEach(function (fp) { 1259 fp.initialHotel = hotel_pk || null; 1260 }); 1261 1262 // Instance URL first; DOM fallback for mobile calendar (no .flatpickr instance). 1263 const calendar_url = 1264 (this.flatpickrInstances[0] && this.flatpickrInstances[0].calendar_url) || 1265 (document.querySelector("[data-calendar-url]") && 1266 document.querySelector("[data-calendar-url]").getAttribute("data-calendar-url")) || 1267 null; 1268 if (kombi.calendarDayPrice) { 1269 return kombi.calendarDayPrice.fetchCalendar(calendar_url, hotel_pk); 1270 } 1271 if (hotel_pk && calendar_url) { 1272 fetch(`${calendar_url}?hotel_pk=${hotel_pk}`) 1273 .then((response) => { 1274 return response.json(); 1275 }) 1276 .then((data) => { 1277 this.flatpickrInstances.forEach((fp) => { 1278 fp.calendar = data; 1279 fp.redraw(); 1280 }); 1281 this.syncAllCalendarLegendVisibility(); 1282 }); 1283 } else { 1284 this.flatpickrInstances.forEach((fp) => { 1285 fp.calendar = null; 1286 fp.redraw(); 1287 }); 1288 this.syncAllCalendarLegendVisibility(); 1289 } 1290}; 1291 1292kombi.openDatePicker = function () { 1293 const flatpickrElements = document.querySelectorAll('.flatpickr'); 1294 let flatpickrElement; 1295 if (window.matchMedia("(min-width: 992px)").matches) { 1296 flatpickrElement = flatpickrElements[0]; 1297 } else { 1298 const modalElement = document.getElementById("SearchForm"); 1299 let modalInstance = bootstrap.Modal.getInstance(modalElement); 1300 if (!modalInstance) { 1301 modalInstance = new bootstrap.Modal(modalElement); 1302 } 1303 modalInstance.show(); 1304 flatpickrElement = flatpickrElements[1]; 1305 } 1306 if (flatpickrElement) { 1307 const flatpickrInstance = flatpickrElement._flatpickr; 1308 if (flatpickrInstance) { 1309 flatpickrInstance.open(); 1310 } else { 1311 flatpickrElement.click(); 1312 } 1313 } 1314};; 1315// Mobile date picker modal (stacked months + day prices), sharing pricing via kombi.calendarDayPrice. 1316kombi.calendarMobile = function () { 1317 const DEFAULT_PACKAGE_LEAD_DAYS = 5; 1318 1319 function getPackageLeadDays() { 1320 var raw = document.body && document.body.dataset 1321 ? document.body.dataset.packageLeadDays 1322 : null; 1323 var parsed = parseInt(raw, 10); 1324 return (parsed === parsed && parsed >= 0) ? parsed : DEFAULT_PACKAGE_LEAD_DAYS; 1325 } 1326 1327 const MAX_ADVANCE_DAYS = 540; 1328 const INITIAL_MONTHS = 2; 1329 const MONTHS_BATCH = 2; 1330 const WEEKDAY_LABELS = ["D", "S", "T", "Q", "Q", "S", "S"]; 1331 const MONTH_NAMES_PT = [ 1332 "Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", 1333 "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro", 1334 ]; 1335 1336 const modalEl = document.getElementById("CalendarModal"); 1337 const mountEl = document.getElementById("calendar-mobile-mount"); 1338 const triggerWrap = document.querySelector("#SearchForm .calendar-mobile-trigger"); 1339 const bodyEl = modalEl && modalEl.querySelector(".calendar-mobile__body"); 1340 1341 if (!modalEl || !mountEl || !triggerWrap || typeof flatpickr !== "function") { 1342 return; 1343 } 1344 1345 const displayInput = triggerWrap.querySelector(".calendar-mobile-display"); 1346 const stayStartInput = triggerWrap.querySelector(".dates-0"); 1347 const stayEndInput = triggerWrap.querySelector(".dates-1"); 1348 const fieldClearButton = triggerWrap.querySelector(".clear-calendar"); 1349 const modalClearButton = modalEl.querySelector(".calendar-mobile__clear"); 1350 const advanceButton = modalEl.querySelector(".calendar-mobile__advance"); 1351 const backButton = modalEl.querySelector(".calendar-mobile__back"); 1352 const legendsEl = modalEl.querySelector(".calendar-mobile__legends"); 1353 const ratesNoteEl = modalEl.querySelector(".calendar-mobile__rates-note"); 1354 1355 let fpInstance = null; 1356 let modalInstance = null; 1357 let loadedMonths = INITIAL_MONTHS; 1358 let totalMonths = INITIAL_MONTHS; 1359 let scrollBeforeSelect = 0; 1360 1361 function isPackageActive() { 1362 return kombi.calendarDayPrice ? kombi.calendarDayPrice.isPackageActive() : false;
1363 } 1364 1365 function getPackageMinDate() { 1366 const d = new Date(); 1367 d.setHours(0, 0, 0, 0); 1368 d.setDate(d.getDate() + getPackageLeadDays() + 1); 1369 return d; 1370 } 1371 1372 function getMinDate() { 1373 return isPackageActive() ? getPackageMinDate() : new Date().fp_incr(1); 1374 } 1375 1376 function getMaxDate() { 1377 return new Date().fp_incr(MAX_ADVANCE_DAYS); 1378 } 1379 1380 function monthsBetween(start, end) { 1381 return ( 1382 (end.getFullYear() - start.getFullYear()) * 12 + 1383 (end.getMonth() - start.getMonth()) + 1384 1 1385 ); 1386 } 1387 1388 1389 function toDateKey(dateObj) { 1390 return kombi.calendarDayPrice ? kombi.calendarDayPrice.toDateKey(dateObj) : ""; 1391 } 1392 1393 // Same as desktop: pass date strings to Flatpickr (dateFormat Y-m-d parses as local). 1394 function parseDefaultDates() { 1395 const raw = (mountEl.getAttribute("data-default-date") || "").split(","); 1396 const parsed = raw 1397 .map(function (value) { 1398 return (value || "").trim(); 1399 }) 1400 .filter(Boolean); 1401 return parsed.length ? parsed : null; 1402 } 1403 1404 // YYYY-MM-DD â DD/MM/YY without Date (avoids UTC midnight skew). 1405 function formatDisplayFromDateKey(dateKey) { 1406 const parts = String(dateKey).split("T")[0].split("-"); 1407 if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) { 1408 return ""; 1409 } 1410 return parts[2] + "/" + parts[1] + "/" + parts[0].slice(-2); 1411 } 1412 1413 function hasSelectedDates() { 1414 if (fpInstance && fpInstance.selectedDates && fpInstance.selectedDates.length > 0) { 1415 return true; 1416 } 1417 const startVal = stayStartInput && stayStartInput.value ? stayStartInput.value.trim() : ""; 1418 const endVal = stayEndInput && stayEndInput.value ? stayEndInput.value.trim() : ""; 1419 return !!(startVal || endVal); 1420 } 1421 1422 function updateClearButtonState() { 1423 if (!modalClearButton) return; 1424 modalClearButton.disabled = !hasSelectedDates(); 1425 } 1426 1427 function syncLegendVisibility() { 1428 const calendar = fpInstance && fpInstance.calendar; 1429 const hasCalendarData = !!(calendar && Object.keys(calendar).length > 0); 1430 const showRatesNote = hasCalendarData && !isPackageActive(); 1431 if (legendsEl) { 1432 legendsEl.classList.toggle("d-none", !hasCalendarData); 1433 legendsEl.setAttribute("aria-hidden", hasCalendarData ? "false" : "true"); 1434 } 1435 if (ratesNoteEl) { 1436 ratesNoteEl.classList.toggle("d-none", !showRatesNote); 1437 ratesNoteEl.setAttribute("aria-hidden", showRatesNote ? "false" : "true"); 1438 } 1439 } 1440 1441 function refreshCalendarUi(instance) { 1442 if (!instance) return; 1443 instance.redraw(); 1444 scheduleMonthHeaders(instance); 1445 syncLegendVisibility(); 1446 updateClearButtonState(); 1447 } 1448 1449 function syncDisplayFromHidden() { 1450 const startVal = stayStartInput && stayStartInput.value ? stayStartInput.value.trim() : ""; 1451 const endVal = stayEndInput && stayEndInput.value ? stayEndInput.value.trim() : ""; 1452 if (startVal && endVal && displayInput) { 1453 const start = formatDisplayFromDateKey(startVal); 1454 const end = formatDisplayFromDateKey(endVal); 1455 if (start && end) { 1456 displayInput.value = start + " até " + end; 1457 if (fieldClearButton) { 1458 fieldClearButton.classList.remove("d-none"); 1459 fieldClearButton.classList.add("d-flex"); 1460 } 1461 updateClearButtonState(); 1462 return; 1463 } 1464 } 1465 if (displayInput) { 1466 displayInput.value = ""; 1467 } 1468 if (fieldClearButton) { 1469 fieldClearButton.classList.add("d-none"); 1470 fieldClearButton.classList.remove("d-flex"); 1471 } 1472 updateClearButtonState(); 1473 } 1474 1475 function writeHiddenDates(selectedDates) { 1476 if (stayStartInput) { 1477 stayStartInput.value = selectedDates[0] ? toDateKey(selectedDates[0]) : ""; 1478 } 1479 if (stayEndInput) { 1480 stayEndInput.value = selectedDates[1] ? toDateKey(selectedDates[1]) : ""; 1481 } 1482 } 1483 1484 function getDaysWrap(instance) { 1485 return instance.calendarContainer && instance.calendarContainer.querySelector(".flatpickr-days"); 1486 } 1487 1488 function buildMonthHeader(monthDate) { 1489 const block = document.createElement("div"); 1490 block.className = "calendar-mobile__month-block"; 1491 1492 const title = document.createElement("div"); 1493 title.className = "calendar-mobile__month-title mb-3"; 1494 const monthSpan = document.createElement("span"); 1495 monthSpan.className = "calendar-mobile__month-name"; 1496 monthSpan.textContent = MONTH_NAMES_PT[monthDate.getMonth()]; 1497 const yearSpan = document.createElement("span"); 1498 yearSpan.className = "calendar-mobile__month-year"; 1499 yearSpan.textContent = " " + monthDate.getFullYear(); 1500 title.appendChild(monthSpan); 1501 title.appendChild(yearSpan); 1502 1503 const weekdays = document.createElement("div"); 1504 weekdays.className = "calendar-mobile__weekdays"; 1505 for (let w = 0; w < WEEKDAY_LABELS.length; w++) { 1506 const span = document.createElement("span"); 1507 span.className = "calendar-mobile__weekday"; 1508 span.textContent = WEEKDAY_LABELS[w]; 1509 weekdays.appendChild(span); 1510 } 1511 1512 block.appendChild(title); 1513 block.appendChild(weekdays); 1514 return block; 1515 } 1516 1517 function renderMonthHeaders(instance) { 1518 const daysWrap = getDaysWrap(instance); 1519 if (!daysWrap) return; 1520 1521 const children = daysWrap.children; 1522 for (let i = children.length - 1; i >= 0; i--) { 1523 if (children[i].classList.contains("calendar-mobile__month-block")) { 1524 children[i].remove(); 1525 } 1526 } 1527 1528 const containers = daysWrap.querySelectorAll(":scope > .dayContainer"); 1529 const baseMonth = instance.currentMonth; 1530 const baseYear = instance.currentYear; 1531 for (let index = 0; index < containers.length; index++) { 1532 const monthDate = new Date(baseYear, baseMonth + index, 1); 1533 daysWrap.insertBefore(buildMonthHeader(monthDate), containers[index]); 1534 } 1535 } 1536 1537 // Re-inject headers once per Flatpickr build (a build wipes them); microtask runs before paint. 1538 function scheduleMonthHeaders(instance) { 1539 if (instance._headerRenderScheduled) return; 1540 instance._headerRenderScheduled = true; 1541 Promise.resolve().then(function () { 1542 instance._headerRenderScheduled = false;
1543 renderMonthHeaders(instance); 1544 }); 1545 } 1546 1547 function resolveHotelPk() { 1548 // Prefer last fetch from search-bar updateCalendar over static detail-page attrs. 1549 if (kombi.calendarDayPrice) { 1550 const lastPk = kombi.calendarDayPrice.getLastHotelPk(); 1551 if (lastPk) { 1552 return lastPk; 1553 } 1554 } 1555 // After clear, updateCalendar removes these attrs so we do not reuse the page hotel. 1556 return ( 1557 mountEl.getAttribute("data-initial-hotel") || 1558 triggerWrap.getAttribute("data-initial-hotel") || 1559 null 1560 ); 1561 } 1562 1563 function resolveCalendarUrl() { 1564 return ( 1565 mountEl.getAttribute("data-calendar-url") || 1566 triggerWrap.getAttribute("data-calendar-url") || 1567 (kombi.flatpickrInstances[0] && kombi.flatpickrInstances[0].calendar_url) || 1568 null 1569 ); 1570 } 1571 1572 function setDateOptionIfChanged(instance, key, dateValue) { 1573 const current = instance.config[key]; 1574 const currentTs = current ? new Date(current).getTime() : null; 1575 const nextTs = dateValue ? new Date(dateValue).getTime() : null; 1576 if (currentTs === nextTs) return; 1577 instance.set(key, dateValue); 1578 } 1579 1580 function resetScrollTop() { 1581 if (bodyEl) bodyEl.scrollTop = 0; 1582 } 1583 1584 function restoreBodyScroll(y) { 1585 if (!bodyEl || y === null || y === undefined) return; 1586 bodyEl.scrollTop = y; 1587 requestAnimationFrame(function () { 1588 bodyEl.scrollTop = y; 1589 // Month headers re-inject on a microtask after Flatpickr rebuild; pin scroll again. 1590 Promise.resolve().then(function () { 1591 bodyEl.scrollTop = y; 1592 }); 1593 }); 1594 } 1595 1596 function getFocusDate(instance) { 1597 const fp = instance || fpInstance; 1598 if (fp && fp.selectedDates && fp.selectedDates.length) { 1599 return fp.selectedDates[0]; 1600 } 1601 const startVal = stayStartInput && stayStartInput.value ? stayStartInput.value.trim() : ""; 1602 if (startVal) { 1603 const fromInput = new Date(startVal); 1604 if (!isNaN(fromInput.getTime())) return fromInput; 1605 } 1606 const defaults = parseDefaultDates(); 1607 return defaults && defaults[0] ? defaults[0] : null; 1608 } 1609 1610 // Flatpickr setDate: put the range back on the instance before sizing the month window. 1611 function syncSelectionFromInputs(instance) { 1612 if (!instance || (instance.selectedDates && instance.selectedDates.length)) return; 1613 const startVal = stayStartInput && stayStartInput.value ? stayStartInput.value.trim() : ""; 1614 const endVal = stayEndInput && stayEndInput.value ? stayEndInput.value.trim() : ""; 1615 if (!startVal || !endVal) return; 1616 const start = new Date(startVal); 1617 const end = new Date(endVal); 1618 if (isNaN(start.getTime()) || isNaN(end.getTime()) || start.getTime() >= end.getTime()) return; 1619 instance.setDate([start, end], false); 1620 } 1621 1622 function monthsNeededThrough(dateObj) { 1623 if (!dateObj) return INITIAL_MONTHS; 1624 // +1 so Flatpickr still shows one month under the selection. 1625 return Math.max(INITIAL_MONTHS, monthsBetween(getMinDate(), dateObj) + 1); 1626 } 1627 1628 // Flatpickr's instance.days is only the first dayContainer â query the whole calendar. 1629 function findFocusDayElement(instance, focusDate) { 1630 if (!instance || !instance.calendarContainer || !focusDate) return null; 1631 const selected = instance.calendarContainer.querySelector( 1632 ".flatpickr-day.startRange, .flatpickr-day.selected" 1633 ); 1634 if (selected && selected.dateObj) { 1635 return selected; 1636 } 1637 const year = focusDate.getFullYear();
1638 const month = focusDate.getMonth(); 1639 const days = instance.calendarContainer.querySelectorAll(".dayContainer .flatpickr-day"); 1640 for (let i = 0; i < days.length; i++) { 1641 const el = days[i]; 1642 if ( 1643 !el.dateObj || 1644 el.classList.contains("prevMonthDay") || 1645 el.classList.contains("nextMonthDay") 1646 ) { 1647 continue; 1648 } 1649 if (el.dateObj.getFullYear() === year && el.dateObj.getMonth() === month) { 1650 return el; 1651 } 1652 } 1653 return null; 1654 } 1655 1656 function scrollBodyToFocusMonth(instance, focusDate) { 1657 if (!bodyEl || !instance || !focusDate) return false; 1658 const dayElem = findFocusDayElement(instance, focusDate); 1659 if (!dayElem) return false; 1660 const dayContainer = dayElem.closest(".dayContainer"); 1661 const prev = dayContainer && dayContainer.previousElementSibling; 1662 const target = 1663 prev && prev.classList.contains("calendar-mobile__month-block") 1664 ? prev 1665 : dayContainer || dayElem; 1666 const bodyRect = bodyEl.getBoundingClientRect(); 1667 const targetRect = target.getBoundingClientRect(); 1668 bodyEl.scrollTop = bodyEl.scrollTop + (targetRect.top - bodyRect.top); 1669 return true; 1670 } 1671 1672 // After Flatpickr rebuild + month-header inject, pin scroll to the selection month. 1673 function scheduleScrollToFocus(instance, focusDate) { 1674 if (!focusDate) { 1675 resetScrollTop(); 1676 return; 1677 } 1678 const run = function () { 1679 if (!scrollBodyToFocusMonth(instance, focusDate)) { 1680 // Days may not exist yet right after set(showMonths); retry once. 1681 requestAnimationFrame(function () { 1682 scrollBodyToFocusMonth(instance, focusDate); 1683 }); 1684 } 1685 }; 1686 Promise.resolve().then(function () { 1687 requestAnimationFrame(run); 1688 }); 1689 } 1690 1691 // jumpToDate(minDate): Flatpickr window always starts at the first bookable month. 1692 function anchorToFirstMonth(instance) { 1693 const minDate = getMinDate(); 1694 if ( 1695 instance.currentYear !== minDate.getFullYear() || 1696 instance.currentMonth !== minDate.getMonth() 1697 ) { 1698 instance.jumpToDate(minDate); 1699 } 1700 } 1701 1702 function syncMonthWindow(instance) { 1703 const minDate = getMinDate(); 1704 const maxDate = getMaxDate(); 1705 totalMonths = Math.max(2, monthsBetween(minDate, maxDate)); 1706 syncSelectionFromInputs(instance); 1707 const focusDate = getFocusDate(instance); 1708 // Flatpickr showMonths grows from minDate through the selected month (earlier months stay). 1709 loadedMonths = Math.min(totalMonths, monthsNeededThrough(focusDate)); 1710 setDateOptionIfChanged(instance, "minDate", minDate); 1711 setDateOptionIfChanged(instance, "maxDate", maxDate); 1712 if (instance.config.showMonths !== loadedMonths) { 1713 instance.set("showMonths", loadedMonths); 1714 } 1715 anchorToFirstMonth(instance); 1716 scheduleScrollToFocus(instance, focusDate); 1717 armLoadMore(); 1718 } 1719 1720 function armLoadMore() { 1721 const observer = bodyEl && bodyEl._loadMoreIO; 1722 const sentinel = bodyEl && bodyEl.querySelector(".calendar-mobile__load-sentinel"); 1723 if (!observer || !sentinel) return; 1724 observer.unobserve(sentinel); 1725 observer.observe(sentinel); 1726 } 1727 1728 function loadMoreMonths() { 1729 if (!fpInstance || loadedMonths >= totalMonths) return; 1730 loadedMonths = Math.min(totalMonths, loadedMonths + MONTHS_BATCH); 1731 const selectingStay = fpInstance.selecting_stay; 1732 fpInstance.set("showMonths", loadedMonths); 1733 fpInstance.selecting_stay = selectingStay; 1734 scheduleMonthHeaders(fpInstance); 1735 if (loadedMonths < totalMonths) { 1736 armLoadMore(); 1737 } 1738 } 1739 1740 // Bottom sentinel drives incremental month loading (works with or without overflow/selection). 1741 function setupLoadMore() { 1742 if (!bodyEl || bodyEl._loadMoreIO) return; 1743 const sentinel = document.createElement("div"); 1744 sentinel.className = "calendar-mobile__load-sentinel"; 1745 bodyEl.appendChild(sentinel); 1746 bodyEl._loadMoreIO = new IntersectionObserver( 1747 function (entries) { 1748 for (let i = 0; i < entries.length; i++) { 1749 if (entries[i].isIntersecting) {
1750 loadMoreMonths(); 1751 break; 1752 } 1753 } 1754 }, 1755 { root: bodyEl, rootMargin: "240px" } 1756 ); 1757 bodyEl._loadMoreIO.observe(sentinel); 1758 } 1759 1760 function loadCalendarData(instance, options) { 1761 const hotelPk = resolveHotelPk(); 1762 const calendarUrl = resolveCalendarUrl(); 1763 const scrollToFocus = !!(options && options.scrollToFocus); 1764 1765 function applyData(data) { 1766 instance.calendar = data; 1767 refreshCalendarUi(instance); 1768 if (scrollToFocus) { 1769 scheduleScrollToFocus(instance, getFocusDate(instance)); 1770 } 1771 } 1772 1773 if ( 1774 kombi.calendarDayPrice && 1775 hotelPk && 1776 kombi.calendarDayPrice.getLastHotelPk() === hotelPk && 1777 kombi.calendarDayPrice.getCachedData() 1778 ) { 1779 applyData(kombi.calendarDayPrice.getCachedData()); 1780 return; 1781 } 1782 1783 if (hotelPk && calendarUrl && kombi.calendarDayPrice) { 1784 kombi.calendarDayPrice.fetchCalendar(calendarUrl, hotelPk).then(applyData); 1785 return; 1786 } 1787 1788 applyData(null); 1789 } 1790 1791 function createFlatpickr() { 1792 const minDate = getMinDate(); 1793 const maxDate = getMaxDate(); 1794 totalMonths = Math.max(2, monthsBetween(minDate, maxDate)); 1795 loadedMonths = Math.min(INITIAL_MONTHS, totalMonths); 1796 const defaultDates = parseDefaultDates(); 1797 1798 fpInstance = flatpickr(mountEl, { 1799 mode: "range", 1800 inline: true, 1801 animate: false, 1802 disableMobile: true, 1803 locale: "pt", 1804 dateFormat: "Y-m-d", 1805 altInput: false, 1806 minDate: minDate, 1807 maxDate: maxDate, 1808 showMonths: loadedMonths, 1809 defaultDate: defaultDates, 1810 monthSelectorType: "static", 1811 onReady: function (selectedDates, dateStr, instance) { 1812 instance.calendarContainer.classList.add("calendar-mobile__fp"); 1813 scheduleMonthHeaders(instance); 1814 updateClearButtonState(); 1815 }, 1816 onMonthChange: function (selectedDates, dateStr, instance) { 1817 scheduleMonthHeaders(instance); 1818 }, 1819 onChange: function (selectedDates, dateStr, instance) { 1820 const y = scrollBeforeSelect; 1821 updateClearButtonState(); 1822 // Check-in stays the anchor after the range closes, so the lazy month load 1823 // rebuilds keep blocking earlier dates and keep the min-stay tip in place. 1824 instance.selecting_stay = selectedDates.length ? selectedDates[0] : null; 1825 anchorToFirstMonth(instance); 1826 instance.redraw(); 1827 scheduleMonthHeaders(instance); 1828 if (document.activeElement && typeof document.activeElement.blur === "function") { 1829 document.activeElement.blur(); 1830 } 1831 restoreBodyScroll(y); 1832 }, 1833 onDayCreate: function (dObj, dStr, fp, dayElem) { 1834 // Flatpickr focuses the picked day; default focus scrolls .calendar-mobile__body. 1835 if (!dayElem._focusNoScroll) { 1836 dayElem._focusNoScroll = true; 1837 const nativeFocus = dayElem.focus.bind(dayElem); 1838 dayElem.focus = function (opts) { 1839 nativeFocus(Object.assign({ preventScroll: true }, opts || {})); 1840 }; 1841 } 1842 kombi.fp_apply_day_rules(fp, dayElem); 1843 scheduleMonthHeaders(fp); 1844 }, 1845 }); 1846 1847 fpInstance.calendar = null; 1848 fpInstance.selecting_stay = null; 1849 loadCalendarData(fpInstance); 1850 setupLoadMore(); 1851 } 1852 1853 function ensureModal() { 1854 if (!modalInstance) { 1855 modalInstance = bootstrap.Modal.getOrCreateInstance(modalEl); 1856 } 1857 return modalInstance; 1858 } 1859 1860 function openMobileCalendar() { 1861 ensureModal().show(); 1862 } 1863 1864 function clearSelection(keepOpen) { 1865 if (!fpInstance) return; 1866 fpInstance.clear(); 1867 fpInstance.selecting_stay = null; 1868 loadedMonths = Math.min(INITIAL_MONTHS, totalMonths); 1869 if (fpInstance.config.showMonths !== loadedMonths) { 1870 fpInstance.set("showMonths", loadedMonths); 1871 } 1872 anchorToFirstMonth(fpInstance); 1873 resetScrollTop(); 1874 writeHiddenDates([]); 1875 syncDisplayFromHidden(); 1876 updateClearButtonState(); 1877 if (keepOpen) { 1878 fpInstance.redraw(); 1879 scheduleMonthHeaders(fpInstance); 1880 } 1881 } 1882 1883 function showSearchAfterHide() { 1884 const searchForm = document.getElementById("SearchForm"); 1885 if (!searchForm) return; 1886 modalEl.addEventListener( 1887 "hidden.bs.modal", 1888 function () { 1889 bootstrap.Modal.getOrCreateInstance(searchForm).show(); 1890 }, 1891 { once: true } 1892 ); 1893 } 1894 1895 function advanceSelection() { 1896 if (!fpInstance) return; 1897 const selected = fpInstance.selectedDates || []; 1898 if (selected.length !== 2 || selected[0].getTime() >= selected[1].getTime()) { 1899 return; 1900 } 1901 writeHiddenDates(selected); 1902 syncDisplayFromHidden(); 1903 showSearchAfterHide(); 1904 ensureModal().hide(); 1905 } 1906 1907 function bindActions() { 1908 if (modalClearButton) { 1909 modalClearButton.addEventListener("click", function () { 1910 if (modalClearButton.disabled) return; 1911 clearSelection(true); 1912 }); 1913 } 1914 if (advanceButton) { 1915 advanceButton.addEventListener("click", advanceSelection); 1916 } 1917 if (backButton) { 1918 // data-bs-dismiss hides the calendar; this reopens the search modal, like Avançar. 1919 backButton.addEventListener("click", showSearchAfterHide); 1920 } 1921 if (bodyEl) { 1922 // Capture before Flatpickr select/focus so we can restore after currentMonth jumps. 1923 bodyEl.addEventListener( 1924 "pointerdown", 1925 function () { 1926 scrollBeforeSelect = bodyEl.scrollTop; 1927 }, 1928 true 1929 ); 1930 } 1931 if (fieldClearButton) { 1932 fieldClearButton.addEventListener("click", function (event) { 1933 event.preventDefault(); 1934 event.stopPropagation(); 1935 clearSelection(false); 1936 }); 1937 } 1938 if (displayInput) { 1939 displayInput.addEventListener("keydown", function (event) { 1940 if (event.key === "Enter" || event.key === " ") { 1941 event.preventDefault();
1942 openMobileCalendar(); 1943 } 1944 }); 1945 } 1946 } 1947 1948 function bindModalLifecycle() { 1949 modalEl.addEventListener("shown.bs.modal", function () { 1950 if (!fpInstance) { 1951 createFlatpickr(); 1952 } 1953 syncMonthWindow(fpInstance); 1954 loadCalendarData(fpInstance, { scrollToFocus: true }); 1955 syncLegendVisibility(); 1956 updateClearButtonState(); 1957 if (kombi.calendarDayPrice) { 1958 kombi.calendarDayPrice.refreshOnCalendarOpen(); 1959 } 1960 }); 1961 } 1962 1963 function bindSearchTabChange() { 1964 document.addEventListener("searchtab:change", function (e) { 1965 const isPackage = e.detail && e.detail.isPackage; 1966 syncLegendVisibility(); 1967 if (!fpInstance) return; 1968 1969 syncMonthWindow(fpInstance); 1970 const minDate = isPackage ? getPackageMinDate() : new Date().fp_incr(1); 1971 const dates = fpInstance.selectedDates || []; 1972 const minTs = minDate.getTime(); 1973 const hasInvalid = dates.some(function (d) { 1974 return d.getTime() < minTs; 1975 }); 1976 if (hasInvalid) { 1977 clearSelection(true); 1978 } 1979 fpInstance.calendar = kombi.calendarDayPrice 1980 ? kombi.calendarDayPrice.getCachedData() 1981 : null; 1982 refreshCalendarUi(fpInstance); 1983 }); 1984 } 1985 1986 const previousOpenDatePicker = kombi.openDatePicker; 1987 kombi.openDatePicker = function () { 1988 if (window.matchMedia("(min-width: 992px)").matches) { 1989 if (typeof previousOpenDatePicker === "function") { 1990 return previousOpenDatePicker.apply(this, arguments); 1991 } 1992 return; 1993 } 1994 const searchModal = document.getElementById("SearchForm"); 1995 if (searchModal) { 1996 bootstrap.Modal.getOrCreateInstance(searchModal).show(); 1997 } 1998 openMobileCalendar(); 1999 }; 2000 2001 kombi.calendarMobile.applyCalendarData = function (data) { 2002 if (!fpInstance) return; 2003 fpInstance.calendar = data; 2004 refreshCalendarUi(fpInstance); 2005 }; 2006 2007 syncDisplayFromHidden(); 2008 syncLegendVisibility(); 2009 updateClearButtonState(); 2010 bindActions(); 2011 bindModalLifecycle(); 2012 bindSearchTabChange(); 2013}; 2014; 2015kombi.paymentPagBank = function () { 2016 let form_payment = document.querySelector("form.form-payment"); 2017 let payment_buttons = document.querySelectorAll(".checkout-pagar-button"); 2018 let pixDetailDiscount = document.querySelector("#pix_discount"); 2019 //Credit Card Fields 2020 let pagbank_cc = document.querySelector("form.form-payment .payment-pagbank-credit-card"); 2021 let creditCardDiv = document.querySelector("#credit-card"); 2022 var pixSection = document.getElementById("pix"); 2023 let totalWithDiscountDiv = document.getElementById("total_with_discounts"); 2024 let totalWithoutDiscountsDiv = document.getElementById("total_without_discounts"); 2025 let totalDiv = document.getElementById("total_div"); 2026 let paymentDiscountSummary = document.querySelectorAll(".price-summary-payment-discount"); 2027 let price_summary_installments = document.querySelectorAll('.price-summary-installments'); 2028 let cc_installments_field = null; 2029 2030 if (pagbank_cc) { 2031 cc_installments_field = pagbank_cc.querySelector("#cc-installments"); 2032 } 2033 2034 if (pixSection) { 2035 pixSection.addEventListener("shown.bs.collapse", function () { 2036 form_payment.addEventListener("submit", function (event) { 2037 const pix_cpf = document.querySelector('#pix-cpf'); 2038 if (pix_cpf.value) { 2039 const validCpf = kombi.cpfValidator(pix_cpf.value); 2040 const pix_cpf_error_message = document.querySelector('#pix-cpf-error-message'); 2041 if (!validCpf) { 2042 event.preventDefault(); 2043 pix_cpf.classList.add('is-invalid'); 2044 pix_cpf_error_message.querySelector("span").textContent = 'Por favor, informe um CPF válido'; 2045 pix_cpf_error_message.style.display = 'block'; 2046 payment_buttons.forEach(function (button) { 2047 kombi.resetButton(button) 2048 if (button.disabled) { 2049 button.disabled = false;
2050 } 2051 }); 2052 } else { 2053 pix_cpf_error_message.style.display = 'none'; 2054 } 2055 } 2056 const pix_full_name = document.querySelector('#pix-full_name'); 2057 const pix_full_name_error_message = document.querySelector('#pix-full-name-error-message'); 2058 if (pix_full_name.value) { 2059 const validFullName = kombi.fullNameValidator(pix_full_name.value); 2060 if (!validFullName) { 2061 event.preventDefault(); 2062 pix_full_name.classList.add('is-invalid'); 2063 pix_full_name_error_message.querySelector("span").textContent = 'Por favor, informe seu nome completo'; 2064 pix_full_name_error_message.style.display = 'block'; 2065 payment_buttons.forEach(function (button) { 2066 kombi.resetButton(button) 2067 if (button.disabled) { 2068 button.disabled = false; 2069 } 2070 }); 2071 } 2072 } else { 2073 pix_full_name_error_message.style.display = 'none'; 2074 } 2075 }); 2076 2077 2078 }); 2079 2080 pixSection.addEventListener("shown.bs.collapse", function () { 2081 cc_installments_field.value = ""; 2082 price_summary_installments.forEach(function (price_summary_installments) { 2083 price_summary_installments.style.display = 'none'; 2084 price_summary_installments.textContent = ""; 2085 }) 2086 if (document.getElementById("discount_pix_value")) { 2087 totalWithDiscountDiv.classList.remove("d-none"); 2088 totalWithDiscountDiv.classList.add("d-flex");
2089 paymentDiscountSummary.forEach(function (element) { 2090 element.classList.remove("d-none"); 2091 element.classList.add("d-flex"); 2092 }); 2093 totalWithoutDiscountsDiv.classList.remove("d-flex"); 2094 totalWithoutDiscountsDiv.classList.add("d-none"); 2095 totalDiv.style.display = "flex"; 2096 totalDiv.style.flexDirection = "column"; 2097 } 2098 }); 2099 2100 pixSection.addEventListener("hidden.bs.collapse", function () { 2101 price_summary_installments.forEach(function (price_summary_installments) { 2102 price_summary_installments.style.display = 'flex'; 2103 }) 2104 if (document.getElementById("discount_pix_value")) { 2105 totalWithDiscountDiv.classList.remove("d-flex"); 2106 totalWithDiscountDiv.classList.add("d-none"); 2107 paymentDiscountSummary.forEach(function (element) { 2108 element.classList.remove("d-flex"); 2109 element.classList.add("d-none"); 2110 }); 2111 totalWithoutDiscountsDiv.classList.remove("d-none"); 2112 totalWithoutDiscountsDiv.classList.add("d-flex"); 2113 totalDiv.style.flexDirection = "row"; 2114 } 2115 2116 let pix_full_name = document.getElementById("pix-full_name"); 2117 let pix_full_name_error_message = document.getElementById("pix-full-name-error-message"); 2118 if (pix_full_name) { 2119 pix_full_name.value = ""; 2120 pix_full_name.classList.remove("is-invalid"); 2121 } 2122 if (pix_full_name_error_message) { 2123 pix_full_name_error_message.style.display = "none"; 2124 } 2125 2126 let pix_cpf = document.getElementById("pix-cpf"); 2127 let pix_cpf_error_message = document.getElementById("pix-cpf-error-message"); 2128 if (pix_cpf) { 2129 pix_cpf.value = ""; 2130 pix_cpf.classList.remove("is-invalid"); 2131 } 2132 if (pix_cpf_error_message) { 2133 pix_cpf_error_message.style.display = "none"; 2134 } 2135 }); 2136 } 2137 if (pagbank_cc) {
2138 let cc_number_field = pagbank_cc.querySelector("#cc-number"); 2139 let cc_encrypted_field = pagbank_cc.querySelector("#id_credit_card-cc_encrypted"); 2140 let cc_bin_field = pagbank_cc.querySelector("#id_credit_card-cc_bin"); 2141 let cc_last_digits_field = pagbank_cc.querySelector("#id_credit_card-cc_last_digits"); 2142 let cc_expiration_month_field = pagbank_cc.querySelector("#id_credit_card-cc_expiration_month"); 2143 let cc_expiration_year_field = pagbank_cc.querySelector("#id_credit_card-cc_expiration_year"); 2144 let cc_final_price = pagbank_cc.querySelector("#cc-final-price"); 2145 let cc_cpf = pagbank_cc.querySelector("#cc-cpf") 2146 let cc_cpf_error_message = document.getElementById("cc-cpf-error-message"); 2147 let pixFullName = document.getElementById("pix-full_name") 2148 let pixCpf = document.getElementById("pix-cpf") 2149 let pix_cpf_error_message = document.getElementById("pix-cpf-error-message"); 2150 form_payment.addEventListener("submit", function (event) { 2151 if (form_payment.checkValidity()) { 2152 payment_buttons.forEach(function (button) { 2153 if (!button.disabled) { 2154 if (!button) { 2155 return; 2156 } 2157 button.classList.add('disabled'); 2158 var buttonText = button.querySelector('.buttonText'); 2159 if (buttonText) { 2160 buttonText.classList.add('d-none'); 2161 } 2162 var spinner = button.querySelector('.buttonSpinner'); 2163 if (spinner) { 2164 spinner.classList.remove('d-none'); 2165 } 2166 } 2167 }); 2168 } 2169 2170 try { 2171 if (PagSeguro && cc_number_field.disabled === false) { 2172 let exp = pagbank_cc.querySelector("#cc-exp").value.split("/"); 2173 2174 let opts = { 2175 holder: pagbank_cc.querySelector("#cc-name").value, 2176 number: kombi.getOnlyDigits(cc_number_field.value), 2177 securityCode: pagbank_cc.querySelector("#cc-csc").value, 2178 expMonth: parseInt(exp[0]), 2179 expYear: 2000 + parseInt(exp[1]), 2180 publicKey: pagbank_cc.querySelector("#payment_public_key").value, 2181 }; 2182 2183 const card = PagSeguro.encryptCard(opts); 2184 const error_mappings = { 2185 "INVALID_SECURITY_CODE": { 2186 errorMessageId: "cc-csc-error-message", 2187 inputId: "cc-csc", 2188 errorMessage: "Por favor, digite um código válido" 2189 }, 2190 "INVALID_EXPIRATION_YEAR": { 2191 errorMessageId: "cc-exp-error-message", 2192 inputId: "cc-exp", 2193 errorMessage: "Por favor, digite uma data válida" 2194 }, 2195 "INVALID_HOLDER": { 2196 errorMessageId: "cc-name-error-message", 2197 inputId: "cc-name", 2198 errorMessage: "Por favor, digite um nome completo" 2199 } 2200 }; 2201 2202 if (card.hasErrors) { 2203 event.preventDefault(); 2204 payment_buttons.forEach(function (button) { 2205 if (button.disabled) { 2206 button.disabled = false; 2207 } 2208 }); 2209 card.errors.forEach(error => { 2210 let error_code = error.code; 2211 if (error_mappings.hasOwnProperty(error_code)) { 2212 let mapping = error_mappings[error_code]; 2213 let error_div = document.getElementById(mapping.errorMessageId); 2214 error_div.querySelector("span").textContent = mapping.errorMessage; 2215 error_div.style.display = 'block'; 2216 let inputField = document.getElementById(mapping.inputId); 2217 inputField.classList.add("is-invalid"); 2218 } 2219 }); 2220 } 2221 2222 cc_encrypted_field.value = card.encryptedCard; 2223 cc_bin_field.value = kombi.getOnlyDigits(cc_number_field.value).slice(0, 6); 2224 cc_last_digits_field.value = kombi.getOnlyDigits(cc_number_field.value).slice(-4); 2225 cc_expiration_month_field.value = parseInt(exp[0]); 2226 cc_expiration_year_field.value = 2000 + parseInt(exp[1]); 2227 2228 if (cc_cpf.value) { 2229 const validCpf = kombi.cpfValidator(cc_cpf.value); 2230 if (!validCpf) { 2231 event.preventDefault(); 2232 cc_cpf.classList.add('is-invalid'); 2233 cc_cpf_error_message.querySelector("span").textContent = 'Por favor, informe um CPF válido'; 2234 cc_cpf_error_message.style.display = 'block'; 2235 payment_buttons.forEach(function (button) { 2236 kombi.resetButton(button) 2237 if (button.disabled) { 2238 button.disabled = false;
2239 } 2240 }); 2241 } else { 2242 cc_cpf_error_message.style.display = 'none'; 2243 } 2244 } 2245 2246 2247 } 2248 } catch (error) { 2249 event.preventDefault(); 2250 console.error(error); 2251 } 2252 }); 2253 2254 cc_number_field.addEventListener("input", function (event) { 2255 let cc_number_error_message = document.getElementById("cc-number-error-message"); 2256 let cc_number_error_message_text = document.getElementById("cc-number-error-message-text"); 2257 let cc_icon = document.getElementById("cc-icon") 2258 let cc_number = kombi.getOnlyDigits(cc_number_field.value); 2259 let cc_final_price = pagbank_cc.querySelector("#cc-final-price"); 2260 2261 if (cc_number.length >= 14) { 2262 if (cc_installments_field.searching) return; 2263 cc_installments_field.searching = true; 2264 let params = new URLSearchParams({ 2265 payment_method: "credit_card", 2266 final_price: parseFloat( 2267 cc_final_price.value.replace(/,/g, ".") 2268 ).toFixed(2), 2269 card_bin: cc_number.slice(0, 6), 2270 }); 2271 let cartId = kombi.getCheckoutCartId(); 2272 if (cartId) { 2273 params.set("cart_id", cartId); 2274 } 2275 2276 cc_installments_field.innerHTML = 2277 "<option value=\"\">Buscando as opções de parcelamento ...</option>"; 2278 fetch("/payment/payment-fees?" + params.toString()) 2279 .then((response) => { 2280 if (!response.ok) { 2281 throw new Error("Network response was not ok"); 2282 } 2283 return (fees = response.json()); 2284 }) 2285 .then((fees) => { 2286 cc_installments_field.innerHTML = "<option value=\"\">Escolha o número de parcelas</option>"; 2287 let installment_plans = fees["installment_plans"]; 2288 installment_plans.forEach((plan) => { 2289 let text = 2290 plan.installments + 2291 "x de R$ " + 2292 kombi.formatAmount(plan.formatted_installment_value) + 2293 " | " + 2294 "Total: R$ " + 2295 kombi.formatAmount(plan.amount.formatted_value); 2296 if (plan.interest_free == true) { 2297 text += " Sem juros!"; 2298 } 2299 2300 var option = document.createElement("option"); 2301 option.value = JSON.stringify(plan); 2302 option.text = text; 2303 cc_installments_field.appendChild(option); 2304 }); 2305 }) 2306 .catch((error) => { 2307 cc_number_field.classList.add("is-invalid"); 2308 cc_number_error_message_text.textContent = 'Por favor, informe um número válido'; 2309 cc_icon.classList.add("d-none") 2310 cc_number_error_message.style.display = 'block'; 2311 console.error("There was a problem with the fetch operation:", error); 2312 }); 2313 } else { 2314 cc_installments_field.innerHTML = 2315 '<option value="">' + 2316 cc_installments_field.getAttribute("data-placeholder") + 2317 "</option>"; 2318 cc_installments_field.searching = false; 2319 if (cc_number_field.classList.contains("is-invalid")) { 2320 cc_icon.classList.remove("d-none") 2321 } 2322 } 2323 }); 2324 cc_installments_field.addEventListener("change", function (event) { 2325 let cc_total_value = document.querySelector('#cc_total_value'); 2326 let cc_total_values = document.querySelectorAll('#cc_total_value'); 2327 let price_summary_total_value_all = document.querySelectorAll('#price_summary_total_value'); 2328 let price_summary_fees = document.querySelectorAll('.price-summary-fees'); 2329 let price_summary_fees_value = document.querySelectorAll('.price-summary-fees-value'); 2330 let price_summary_fees_label = document.querySelectorAll('.price-summary-fees-label'); 2331 let price_summary_installments = document.querySelectorAll('.price-summary-installments'); 2332 let cc_final_price = document.querySelector("#cc-final-price"); 2333 if (cc_installments_field.value) { 2334 installments_json = JSON.parse(cc_installments_field.value); 2335 let total_amount = parseFloat(cc_final_price.value.replace(/,/g, ".")); 2336 let total_amount_with_fees = installments_json.amount.value / 100; 2337 let total_fees = total_amount_with_fees - total_amount; 2338 2339 let installment_number_str = installments_json.installments.toString(); 2340
2341 price_summary_installments.forEach(function (price_summary_installments) { 2342 price_summary_installments.style.display = ''; 2343 price_summary_installments.textContent = kombi.template.render( 2344 installments_json.interest_free ? 2345 price_summary_installments.dataset['withoutFeesText'] : 2346 price_summary_installments.dataset['withFeesText'], 2347 { 2348 'installment_number_str': installment_number_str, 2349 'installment_value_str': installments_json.formatted_installment_value, 2350 } 2351 ); 2352 }); 2353 2354 if (total_fees >= 0.01) { 2355 price_summary_fees.forEach(function (element) { 2356 element.style.display = ""; 2357 }); 2358 price_summary_fees_value.forEach(function (element) { 2359 element.style.display = ""; 2360 var currencySymbol = document.getElementById('currency-symbol').textContent; 2361 element.textContent = currencySymbol + new Intl.NumberFormat( 2362 'pt-BR', 2363 { minimumFractionDigits: 2, maximumFractionDigits: 2 } 2364 ).format(total_fees); 2365 }); 2366 price_summary_fees_label.forEach(function (element) { 2367 element.style.display = ""; 2368 }); 2369 } else { 2370 price_summary_fees.forEach(function (element) { 2371 element.style.display = "none"; 2372 }); 2373 price_summary_fees_value.forEach(function (element) { 2374 element.style.display = "none"; 2375 }); 2376 price_summary_fees_label.forEach(function (element) { 2377 element.style.display = "none"; 2378 }); 2379 }; 2380 2381 let formattedTotal = new Intl.NumberFormat( 2382 'pt-BR', 2383 { minimumFractionDigits: 2, maximumFractionDigits: 2 } 2384 ).format(total_amount_with_fees); 2385 cc_total_values.forEach(function (el) { el.textContent = formattedTotal; }); 2386 } else { 2387 price_summary_installments.forEach(function (price_summary_installments) { 2388 price_summary_installments.textContent = ''; 2389 }); 2390 } 2391 2392 try { 2393 price_summary_total_value_all.forEach(element => { 2394 element.textContent = cc_total_value.textContent; 2395 }); 2396 } catch (error) { 2397 console.error(error); 2398 } 2399 2400 }); 2401 2402 if (creditCardDiv) { 2403 creditCardDiv.addEventListener("hidden.bs.collapse", function () { 2404 let cc_final_price = document.querySelector("#cc-final-price"); 2405 if (!cc_final_price) return; 2406 2407 let original_total = parseFloat(cc_final_price.value.replace(/,/g, ".")); 2408 let formattedTotal = new Intl.NumberFormat( 2409 'pt-BR', 2410 { minimumFractionDigits: 2, maximumFractionDigits: 2 } 2411 ).format(original_total); 2412 2413 document.querySelectorAll('#cc_total_value').forEach(function (el) { 2414 el.textContent = formattedTotal; 2415 }); 2416 2417 document.querySelectorAll('#price_summary_total_value').forEach(function (el) { 2418 el.textContent = formattedTotal; 2419 }); 2420 2421 document.querySelectorAll('.price-summary-installments').forEach(function (el) { 2422 el.textContent = ''; 2423 el.style.display = 'none'; 2424 }); 2425 2426 document.querySelectorAll('.price-summary-fees').forEach(function (el) { 2427 el.style.display = 'none'; 2428 }); 2429 2430 document.querySelectorAll('.price-summary-fees-value').forEach(function (el) { 2431 el.style.display = 'none'; 2432 }); 2433 2434 document.querySelectorAll('.price-summary-fees-label').forEach(function (el) { 2435 el.style.display = 'none'; 2436 }); 2437 2438 creditCardDiv.querySelectorAll('.cc-input').forEach(function (field) { 2439 if (field.tagName === 'SELECT') { 2440 field.innerHTML = 2441 '<option value="">' + 2442 field.getAttribute('data-placeholder') + 2443 '</option>'; 2444 } else { 2445 field.value = ''; 2446 } 2447 field.classList.remove('is-invalid'); 2448 let errorMessage = document.getElementById(field.id + '-error-message'); 2449 if (errorMessage) { 2450 errorMessage.style.display = 'none'; 2451 } 2452 }); 2453 2454 let cc_icon = document.getElementById('cc-icon'); 2455 if (cc_icon) { 2456 cc_icon.classList.remove('d-none'); 2457 } 2458 }); 2459 } 2460 2461 if (!pixSection) { 2462 let buttons = document.querySelectorAll("body.checkout [data-bs-target='#credit-card'], body.payment-view [data-bs-target='#credit-card']"); 2463 2464 buttons.forEach(button => { 2465 button.removeAttribute("data-bs-toggle"); 2466 button.removeAttribute("onclick"); 2467 button.classList.remove("collapsed"); 2468 }); 2469 2470 let cc_collapse = new bootstrap.Collapse(creditCardDiv, { toggle: false }); 2471 2472 cc_collapse.show(); 2473 } 2474 } 2475 if (form_payment) { 2476 let input_fields = form_payment.querySelectorAll("input"); 2477 input_fields.forEach(input_field => { 2478 input_field.addEventListener("input", function () { 2479 if (this.classList.contains("is-invalid")) { 2480 this.classList.remove("is-invalid"); 2481 let errorDivId = this.id + "-error-message"; 2482 let errorDiv = document.getElementById(errorDivId); 2483 if (errorDiv) { 2484 errorDiv.style.display = "none"; 2485 } 2486 } 2487 }); 2488 }); 2489 } 2490}
2491kombi.formatAmount = function (amount) { 2492 let number = parseFloat(amount.replace(",", ".")); 2493 return new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number); 2494} 2495 2496kombi.isCollapseOpen = function(collapse_id) { 2497 let collapse = document.getElementById(collapse_id); 2498 return collapse.classList.contains("show") || collapse.classList.contains("collapsing"); 2499}; 2500kombi.activeLinks = function () { 2501 let found = false; 2502 document.querySelectorAll(".nav-link[href='" + location.pathname + "']").forEach((item) => { 2503 found = true; 2504 item.classList.add("active"); 2505 }); 2506 2507 if (!found) { 2508 if (location.pathname.indexOf("/hotel/") == 0){ 2509 document.querySelectorAll(".nav-link[href='/hotel/']").forEach((item) => { 2510 item.classList.add("active"); 2511 }); 2512 } 2513 } 2514 2515 2516}; 2517; 2518kombi.currentYear = function() { 2519 function getYear() { 2520 const date = new Date() 2521 const year = date.getFullYear() 2522 return year 2523 } 2524 const yearElement = document.getElementById('year') 2525 const year = getYear() 2526 if (yearElement) { 2527 yearElement.textContent = year 2528 } 2529} 2530 2531kombi.inputNumberPlusMinusBtn = function() { 2532 var numberInput = document.getElementById('numberInput') 2533 var plusBtn = document.getElementById('plusBtn') 2534 var minusBtn = document.getElementById('minusBtn') 2535 2536 plusBtn?.addEventListener('click', function () { 2537 numberInput.value = parseInt(numberInput.value) + 1 2538 }) 2539 minusBtn?.addEventListener('click', function () { 2540 if (parseInt(numberInput.value) > 0) { 2541 numberInput.value = parseInt(numberInput.value) - 1 2542 } 2543 }) 2544} 2545; 2546kombi.requests = { 2547 request: function (url, method, headers, body) { 2548 return fetch(url, { 2549 method: method.toUpperCase(), 2550 headers: headers, 2551 body: body 2552 }) 2553 }, 2554 postFormJson: async function (url, form, csrfmiddlewaretoken) { 2555 try { 2556 if (!form.__disabled) { 2557 form.__disabled = true 2558 const postData = new FormData(form) 2559 const csrfToken = csrfmiddlewaretoken || postData.get('csrfmiddlewaretoken') 2560 const headers = { 2561 'Content-Type': 'application/json', 2562 'X-CSRFToken': csrfToken 2563 } 2564 const response = await this.request( 2565 url, 2566 'POST', 2567 headers, 2568 JSON.stringify(Object.fromEntries(postData.entries())) 2569 ).catch((error) => { 2570 console.error("There has been a problem with your fetch operation:", error); 2571 }); 2572 2573 form.__disabled = false 2574 return await response.json(); 2575 } else { 2576 console.info("postFormJson is running, wait", form); 2577 } 2578 } catch (error) { 2579 form.__disabled = false 2580 console.error("There has been a problem with your postFormJson", error); 2581 } 2582 }, 2583 getText: async function (url) { 2584 try { 2585 2586 let response = await this.request(url, 'GET') 2587 .catch((error) => { 2588 console.error("There has been a problem with your fetch operation:", error); 2589 }); 2590 2591 return await response.text(); 2592 } catch (error) { 2593 console.error("There has been a problem with your getText", error); 2594 } 2595 } 2596} 2597; 2598kombi.alert = function (parent, message, type, clear_all) { 2599 const wrapper = document.createElement('div') 2600 wrapper.innerHTML = [ 2601 `<div class="alert alert-${type} fade show alert-dismissible" role="alert">`, 2602 ` <div>${message}</div>`, 2603 ' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>', 2604 '</div>' 2605 ].join('') 2606 if (clear_all) { 2607 parent.replaceChildren(wrapper) 2608 } else { 2609 parent.append(wrapper) 2610 } 2611 2612 setTimeout(() => { 2613 wrapper.querySelector(".btn-close").click() 2614 }, 5000) 2615} 2616 2617kombi.popFloatAlert = function (message, type) { 2618 type = type === 'error' ? 'danger' : type; 2619 const floatMessageBox = document.getElementById('FloatMessageContainer'); 2620 floatMessageBox.innerHTML = ` 2621 <div class="alert alert-${type} float-alert text-center" role="alert"> 2622 ${message} 2623 </div> 2624 `; 2625}; 2626kombi.template = { 2627 render: function (text, context) { 2628 let keys = Object.keys(context); 2629 let values = Object.values(context); 2630 return new Function(...keys, `return \`${text}\`;`)(...values); 2631 }, 2632 render_from_url: function (url, context){ 2633 try { 2634 let self = this; 2635 return kombi.requests.getText(url).then((text) => { 2636 return self.render(text, context); 2637 }) 2638 } catch (error) { 2639 console.error("There has been a problem with your render_from_url", error); 2640 } 2641 } 2642} 2643; 2644kombi.reservation = { 2645 navLinkActiveStyle: function() { 2646 let activeLink = document.querySelector('.reservation .nav-link.active'); 2647 let parents = document.querySelectorAll('.reservation .nav-item'); 2648
2649 parents.forEach(function(parent) { 2650 parent.classList.remove('active-parent'); 2651 }); 2652 if (activeLink) { 2653 activeLink.parentElement.classList.add('active-parent'); 2654 } 2655 } 2656 // modal: function() { 2657 // const myReservations = document.querySelector('body.dashboard'); 2658 // const modal = document.querySelector('#myReservationsModal'); 2659 // if (myReservations && modal) { 2660 // var modal_my_reservations = new bootstrap.Modal('#myReservationsModal'); 2661 // modal_my_reservations.show() 2662 // } 2663 // } 2664} 2665 2666; 2667kombi.profile = { 2668 profileImage: function () { 2669 let inputFile = document.getElementById("id_image_upload") 2670 inputFile.addEventListener("change", function() { 2671 document.getElementById("submit_image").click() 2672 }) 2673 }, 2674 edit: function (customClass) { 2675 let editElement = document.querySelector('.edit' + customClass); 2676 let saveElement = document.querySelector('.save' + customClass); 2677 if (editElement) { 2678 saveElement.classList.remove('d-flex'); 2679 saveElement.classList.add('d-none'); 2680 editElement.classList.remove('d-none'); 2681 editElement.classList.add('d-flex'); 2682 } 2683 }, 2684 return: function (customClass) { 2685 let editElement = document.querySelector('.edit' + customClass); 2686 let saveElement = document.querySelector('.save' + customClass); 2687 if (editElement) { 2688 saveElement.classList.remove('d-none'); 2689 saveElement.classList.add('d-flex'); 2690 editElement.classList.remove('d-flex'); 2691 editElement.classList.add('d-none'); 2692 } 2693 }, 2694 formatPhoneNumber: function () { 2695 let phoneNumberElement = document.getElementById('phone_number') 2696 if (phoneNumberElement) { 2697 let phoneNumber = phoneNumberElement.textContent.trim(); 2698 if (phoneNumber.startsWith('+55')) { 2699 let numberWithoutDDD = phoneNumber.slice(3).replace(/\D/g, ''); 2700 let formattedNumber = numberWithoutDDD.replace(/^(\d{2})(\d{5})(\d{4})$/, '+55 ($1) $2-$3'); 2701 phoneNumberElement.textContent = formattedNumber; 2702 } 2703 } 2704 2705 let phoneInput = document.getElementById('id_phone_number'); 2706 let errorMessage = document.getElementById('error-message'); 2707 let errorMessageText = document.getElementById('error-message-text'); 2708 if (phoneInput) { 2709 let lastValue = phoneInput.value; 2710 phoneInput.addEventListener('input', kombi.dddValidation(phoneInput, errorMessage, errorMessageText, lastValue)); 2711 } 2712 }, 2713}; 2714kombi.disableCollapseFields = function () { 2715 let toggleFields = function (item, disable) { 2716 item.querySelectorAll("input, select, textarea, button").forEach((field) => { 2717 field.disabled = disable; 2718 }); 2719 } 2720 document.querySelectorAll(".disable-collapse-fields").forEach((item) => { 2721 item.addEventListener('hidden.bs.collapse', event => { 2722 toggleFields(item, true) 2723 }); 2724 item.addEventListener('show.bs.collapse', event => { 2725 toggleFields(item, false) 2726 }); 2727 2728 toggleFields(item, !item.classList.contains("show")) 2729 }); 2730} 2731; 2732kombi.travelersFormValidation = function () { 2733 2734 let form = document.querySelector("form.form-payment"); 2735 2736 if (form !== null) { 2737 let formElements = Array.from(form.elements); 2738 2739 formElements.forEach(function (element) { 2740 if (element.tagName == 'INPUT' && element.name.startsWith('item_')) { 2741 let fieldInput = document.getElementById(element.id); 2742 2743 if (fieldInput) { 2744 fieldInput.addEventListener('input', function () { 2745 if (fieldInput.classList.contains('is-invalid')) { 2746 fieldInput.classList.remove('is-invalid'); 2747 } 2748 }); 2749 if (fieldInput.id.includes('phone')) { 2750 const errorMessage = document.getElementById('error-message'); 2751 const errorMessageText = document.getElementById('error-message-text'); 2752 2753 let lastValue = fieldInput.value; 2754 2755 fieldInput.addEventListener('input', kombi.dddValidation(fieldInput, errorMessage, errorMessageText, lastValue)); 2756 2757 }; 2758 }; 2759 }; 2760 2761 }); 2762 } 2763}; 2764kombi.checkoutTimer = { 2765 startTimer: function (timeStampLimit, display) { 2766 var timerInterval = setInterval(function () { 2767 var timeStampNow = parseInt(new Date().getTime() / 1000); 2768 var timer = timeStampLimit - timeStampNow; 2769 if (timer < 0) { timer = 0 }; 2770 minutes = parseInt(timer / 60, 10); 2771 seconds = parseInt(timer % 60, 10); 2772 minutes = minutes < 10 ? "0" + minutes : minutes; 2773 seconds = seconds < 10 ? "0" + seconds : seconds; 2774 display.textContent = minutes + ":" + seconds; 2775 if (--timer < 0) { 2776 clearInterval(timerInterval); 2777 document.querySelectorAll(".checkout-pagar-button").forEach(function(button) { 2778 button.setAttribute("disabled", true); 2779 }); 2780 kombi.checkoutTimer.loadModal(); 2781 } 2782 }, 1000); 2783 }, 2784 setTimer: function () { 2785 var displays = document.querySelectorAll('.checkout_timer'); 2786 if (displays.length > 0) { 2787 displays.forEach(function(display) { 2788 var timeStampLimit = parseInt(display.getAttribute('offer_timestamp_limit')); 2789 kombi.checkoutTimer.startTimer(timeStampLimit, display); 2790 }); 2791 } 2792 },
2793 loadModal: function () { 2794 const expiredPixModal = document.querySelector('#expiredPixModal'); 2795 if (expiredPixModal) { 2796 const modal_expired_pix = new bootstrap.Modal('#expiredPixModal'); 2797 modal_expired_pix.show(); 2798 kombi.checkoutTimer.expiredPix(); 2799 } else { 2800 var payment = document.querySelector('#modal-unavailable'); 2801 if (payment && payment.getAttribute('ispaymentunavailable') === 'True') { 2802 var modal_unavailable = new bootstrap.Modal('#modal-unavailable', { 2803 backdrop: 'static', 2804 keyboard: false, 2805 }); 2806 modal_unavailable.show() 2807 } else { 2808 var modal_counter = new bootstrap.Modal('#modal-counter', { 2809 backdrop: 'static', 2810 keyboard: false, 2811 }); 2812 modal_counter.show() 2813 } 2814 } 2815 }, 2816 checkAvailability: function () { 2817 var available = document.querySelector('#modal-unavailable'); 2818 if (available && (available.getAttribute('isunavailable') === 'True' || available.getAttribute('ispaymentunavailable') === 'True')) { 2819 var modal_unavailable = new bootstrap.Modal('#modal-unavailable', { 2820 backdrop: 'static', 2821 keyboard: false, 2822 }); 2823 modal_unavailable.show() 2824 } else { 2825 kombi.checkoutTimer.setTimer(); 2826 } 2827 }, 2828 fixedDiv: function () { 2829 let headerHeight; 2830 let divHeader = document.querySelector('header'); 2831 if (divHeader) { 2832 headerHeight = document.querySelector('header').offsetHeight; 2833 } 2834 let offerAlert = document.querySelector('.offer-alert'); 2835 if (offerAlert) { 2836 let checkScroll = function () { 2837 if (window.innerWidth < 576) { 2838 if (window.scrollY > headerHeight) { 2839 offerAlert.classList.add('fixed-div'); 2840 } else { 2841 offerAlert.classList.remove('fixed-div'); 2842 } 2843 } 2844 }; 2845 window.addEventListener('scroll', checkScroll); 2846 } 2847 }, 2848 expiredPix: function () { 2849 const pixText = document.querySelector('.pix-text'); 2850 const pixImages = document.querySelectorAll('.pix-img'); 2851 pixText.textContent = ''; 2852 pixText.classList.add('py-3'); 2853 pixImages.forEach(pixImg => { 2854 pixImg.remove(); 2855 }); 2856 } 2857} 2858 2859; 2860kombi.dddValidation = function (phoneInput, errorMessage, errorMessageText, lastValue) { 2861 return function() { 2862 let currentValue = phoneInput.value; 2863 if (currentValue === lastValue && currentValue.length > 3 && currentValue.length < 8) { 2864 errorMessageText.textContent = 'Por favor, informe um DDD válido'; 2865 errorMessage.style.display = 'block'; 2866 } else { 2867 errorMessage.style.display = 'none'; 2868 } 2869 lastValue = currentValue; 2870 } 2871}; 2872kombi.cpfValidator = function (cpf) { 2873 if (cpf == null) { 2874 return false; 2875 } 2876 cpf = cpf.replace(/\D/g, ''); 2877 if (cpf.length !== 11) { 2878 return false; 2879 } 2880 if (/^(\d)\1{10}$/.test(cpf)) { 2881 return false; 2882 } 2883 let sum = 0; 2884 let remainder; 2885 for (let i = 1; i <= 9; i++) { 2886 sum += parseInt(cpf.charAt(i - 1)) * (11 - i); 2887 } 2888 remainder = (sum * 10) % 11; 2889 remainder = (remainder === 10 || remainder === 11) ? 0 : remainder; 2890 if (remainder !== parseInt(cpf.charAt(9))) { 2891 return false; 2892 } 2893 sum = 0; 2894 for (let i = 1; i <= 10; i++) { 2895 sum += parseInt(cpf.charAt(i - 1)) * (12 - i); 2896 } 2897 remainder = (sum * 10) % 11; 2898 remainder = (remainder === 10 || remainder === 11) ? 0 : remainder; 2899 return remainder === parseInt(cpf.charAt(10)); 2900}; 2901kombi.sidebar = { 2902 collapseToggle: function() { 2903 let navItems = document.querySelectorAll('.sidebar .nav-item'); 2904 navItems.forEach(item => { 2905 let isActive = item.querySelector('.nav-link').classList.contains('active'); 2906 if (isActive) { 2907 let editImageBox = document.querySelector('.edit-image-box'); 2908 if (item.classList.contains('profile-link')) { 2909 editImageBox.classList.add('d-block'); 2910 editImageBox.classList.remove('d-none'); 2911 } 2912 } 2913 }); 2914 }, 2915 menuLinkActive: function() { 2916 let menuLinks = document.querySelectorAll('.menu-links'); 2917 menuLinks.forEach(link => { 2918 if(window.location.href == link.href) { 2919 link.classList.add('text-primary'); 2920 } 2921 }); 2922 }, 2923 mobileDropdownModal: function() { 2924 let dropdownToggles = document.querySelectorAll('.nav-link.dropdown-toggle'); 2925 2926 if (dropdownToggles) {
2927 dropdownToggles.forEach(function(toggle) { 2928 toggle.addEventListener('click', function(event) { 2929 if (window.innerWidth < 576) { 2930 let dropdownMenu = this.closest('.nav-item').querySelector('.dropdown-menu'); 2931 let menuContent = dropdownMenu.innerHTML; 2932 let menuTitle = toggle.textContent || toggle.innerText; 2933 2934 let modalBody = document.querySelector('#mobileDropdownModal .modal-body ul'); 2935 modalBody.innerHTML = menuContent; 2936 let modalTitle = document.querySelector('#mobileDropdownModal .modal-title'); 2937 modalTitle.textContent = menuTitle.trim(); 2938 2939 let modal = new bootstrap.Modal(document.getElementById('mobileDropdownModal')); 2940 modal.show(); 2941 } 2942 }); 2943 }); 2944 } 2945 }, 2946 navBarFade: function() { 2947 document.querySelectorAll('.fade-container').forEach(function(fadeContainer) { 2948 let fadeEnd = fadeContainer.querySelector('.fade-end'); 2949 let fadeElement = fadeContainer.querySelector('.fade-element'); 2950 2951 function updateFadePosition() { 2952 let maxScrollLeft = fadeEnd.scrollWidth - fadeEnd.clientWidth; 2953 let currentScroll = fadeEnd.scrollLeft; 2954 let fadeWidth = 100; 2955 if (currentScroll >= maxScrollLeft) { 2956 fadeElement.style.display = 'none'; 2957 } else { 2958 fadeElement.style.display = ''; 2959 fadeElement.style.width = `${Math.min(fadeWidth, maxScrollLeft - currentScroll)}px`; 2960 } 2961 } 2962 2963 fadeEnd.addEventListener('scroll', updateFadePosition); 2964 2965 updateFadePosition(); 2966 }); 2967 } 2968}; 2969kombi.reloadInstallments = function () {
2970 let cc_number_field = document.getElementById("cc-number"); 2971 if (!cc_number_field) { 2972 return; 2973 } 2974 let cc_number_error_message = document.getElementById("cc-number-error-message"); 2975 let cc_number_error_message_text = document.getElementById("cc-number-error-message-text"); 2976 let price_summary_installments = document.querySelectorAll('.price-summary-installments'); 2977 let price_summary_total_value_all = document.querySelectorAll('#price_summary_total_value'); 2978 let cc_total_value = document.querySelector('#cc_total_value'); 2979 let cc_icon = document.getElementById("cc-icon") 2980 let cc_number = kombi.getOnlyDigits(cc_number_field.value); 2981 let cc_installments_field = document.getElementById("cc-installments"); 2982 let cc_final_price = document.getElementById("cc-final-price"); 2983 price_summary_total_value_all.forEach(element => { 2984 element.textContent = cc_total_value.textContent; 2985 }); 2986 if (cc_number.length >= 14) { 2987 let params = new URLSearchParams({ 2988 payment_method: "credit_card", 2989 final_price: parseFloat( 2990 cc_final_price.value.replace(/,/g, ".") 2991 ).toFixed(2), 2992 card_bin: cc_number.slice(0, 6), 2993 }); 2994 let cartId = kombi.getCheckoutCartId(); 2995 if (cartId) { 2996 params.set("cart_id", cartId); 2997 } 2998 cc_installments_field.innerHTML = 2999 "<option value=\"\">Buscando as opções de parcelamento ...</option>"; 3000 fetch("/payment/payment-fees?" + params.toString()) 3001 .then((response) => { 3002 if (!response.ok) { 3003 throw new Error("Network response was not ok"); 3004 } 3005 return (fees = response.json()) 3006 }) 3007 .then((fees) => { 3008 cc_installments_field.innerHTML = "<option value=\"\">Escolha o número de parcelas</option>"; 3009 let installment_plans = fees["installment_plans"]; 3010 installment_plans.forEach((plan) => { 3011 let text = 3012 plan.installments + 3013 "x de R$ " + 3014 kombi.formatAmount(plan.formatted_installment_value) + 3015 " | " + 3016 "Total: R$ " + 3017 kombi.formatAmount(plan.amount.formatted_value); 3018 if (plan.interest_free == true) { 3019 text += " Sem juros!"; 3020 } 3021 3022 var option = document.createElement("option"); 3023 option.value = JSON.stringify(plan); 3024 option.text = text; 3025 cc_installments_field.appendChild(option); 3026 }); 3027 }) 3028 .catch((error) => { 3029 cc_number_field.classList.add("is-invalid"); 3030 cc_number_error_message_text.textContent = 'Por favor, informe um número válido'; 3031 cc_icon.classList.add("d-none") 3032 cc_number_error_message.style.display = 'block'; 3033 console.error("There was a problem with the fetch operation:", error); 3034 }); 3035 price_summary_installments.forEach(function (price_summary_installments) { 3036 price_summary_installments.textContent = ''; 3037 }); 3038 3039 } else { 3040 cc_installments_field.innerHTML = 3041 '<option value="">' + 3042 cc_installments_field.getAttribute("data-placeholder") + 3043 "</option>"; 3044 cc_installments_field.searching = false; 3045 if (cc_number_field.classList.contains("is-invalid")) { 3046 cc_icon.classList.remove("d-none") 3047 } 3048 } 3049}; 3050kombi.syncInstallmentNoInterestTag = function (conditions) { 3051 if (!conditions) { 3052 return; 3053 } 3054 3055 const maxInstallments = Number(conditions.max_installments) || 0; 3056 if (maxInstallments > 0) { 3057 const maxLabel = document.getElementById("installment-max-label"); 3058 if (maxLabel) { 3059 maxLabel.textContent = "Parcelamento em até " + maxInstallments + " vezes"; 3060 } 3061 const maxLabelBottom = document.getElementById("installment-max-label-bottom"); 3062 if (maxLabelBottom) { 3063 maxLabelBottom.textContent = "Até " + maxInstallments + "x no cartão"; 3064 } 3065 } 3066 3067 const tag = document.getElementById("installment-no-interest-tag"); 3068 const valueEl = document.getElementById("installment_no_interest_value"); 3069 if (!tag || !valueEl) { 3070 return; 3071 } 3072 const noInterest = Number(conditions.no_interest_installments) || 0; 3073 if (noInterest > 1) { 3074 valueEl.textContent = noInterest + "x sem juros"; 3075 tag.classList.remove("d-none"); 3076 } else { 3077 valueEl.textContent = ""; 3078 tag.classList.add("d-none"); 3079 } 3080}; 3081 3082// Template may assign conditions before this deferred bundle runs. 3083if (window.kombiInstallmentConditions) { 3084 kombi.syncInstallmentNoInterestTag(window.kombiInstallmentConditions); 3085} 3086; 3087kombi.sign = { 3088 displayErrors: function (error_list, form_id, is_raw = false, add_valid_tag = false) { 3089 /**
3090 * Display the error messages in the form 3091 * @param {object} error_list Object with the error messages 3092 * @param {string} form_id The form id to display the errors 3093 * @param {boolean} is_raw If the error_list is a django form error in html format 3094 * @param {boolean} add_valid_tag Add the is-valid class to the inputs that don't have errors 3095 * @returns {boolean} 3096 */ 3097 function parseErrorString(errorString) { 3098 const regex = /<li>([^<]+)<ul class="errorlist"><li>([^<]+)/g; 3099 const matches = {}; 3100 let match; 3101 3102 while ((match = regex.exec(errorString)) !== null) { 3103 const [, key, value] = match; 3104 if (!matches[key]) { 3105 matches[key] = []; 3106 } 3107 matches[key].push(value); 3108 } 3109 3110 return matches; 3111 } 3112 3113 if (is_raw) { 3114 error_list = parseErrorString(error_list); 3115 console.log(error_list); 3116 } 3117 3118 let isValid = true; 3119 const form = document.getElementById(form_id); 3120 const all_inputs = form.querySelectorAll("input, select"); 3121 Object.keys(error_list).forEach((field) => { 3122 const errorMessages = error_list[field]; 3123 const formControl = form.querySelector("[name=" + field + "]"); 3124 formControl.classList.add("is-invalid"); 3125 formControl.classList.add("d-block"); 3126 const invalidFeedback = formControl.parentElement.querySelector(".invalid-feedback"); 3127 if (invalidFeedback) { 3128 invalidFeedback.innerHTML = errorMessages.join("<br>"); 3129 } 3130 isValid = false; 3131 }); 3132 3133 3134 if (add_valid_tag) { 3135 all_inputs.forEach((input) => { 3136 if (!error_list.hasOwnProperty(input.name)) { 3137 const formControl = form.querySelector("[name=" + input.name + "]"); 3138 formControl.classList.add("is-valid"); 3139 } 3140 }); 3141 } 3142 3143 return isValid; 3144 }, 3145 3146 cleanValidations: function (form_id) { 3147 /** 3148 * Clean the validation classes from the form 3149 * @param {string} form_id The form id to clean 3150 * @returns {void} 3151 * 3152 */ 3153 3154 const form = document.getElementById(form_id); 3155 const all_inputs = form.querySelectorAll("input, select"); 3156 all_inputs.forEach((input) => { 3157 if (input.type === "hidden") { 3158 return; 3159 } 3160 input.classList.remove("is-invalid"); 3161 input.classList.remove("is-valid"); 3162 const invalidFeedback = input.parentElement.querySelector(".invalid-feedback"); 3163 if (invalidFeedback) { 3164 invalidFeedback.innerHTML = ""; 3165 } 3166 const formControl = input.parentElement.querySelector(".form-control"); 3167 if (formControl) { 3168 formControl.classList.remove("is-invalid"); 3169 formControl.classList.remove("is-valid"); 3170 } 3171 }); 3172 }, 3173 3174 clearFormFields: function (form_id) { 3175 /** 3176 * Clear the form fields 3177 * @param {string} form_id The form id to clear 3178 * @returns {void} 3179 * 3180 */ 3181 const form = document.getElementById(form_id); 3182 form.classList.remove("was-validated"); 3183 form.__disabled = false; 3184 const all_inputs = form.querySelectorAll("input, select"); 3185 all_inputs.forEach((input) => { 3186 if (input.type === "hidden") { 3187 return; 3188 } 3189 input.value = ""; 3190 input.classList.remove("is-invalid"); 3191 input.classList.remove("is-valid"); 3192 }); 3193 }, 3194 3195 emailValidation: function (email) { 3196 if (email.trim() === '') { 3197 return false; 3198 } 3199 const emailReg = /^([\w-.+]+@([\w-]+\.)+[\w-]{2,10})?$/; 3200 return emailReg.test(email); 3201 }, 3202}; 3203kombi.loginModalAutoOpen = { 3204 _timerId: null, 3205 3206 init: function () { 3207 const metaTag = document.querySelector('meta[name="login-modal-appear-time"]'); 3208 if (!metaTag) { 3209 return; 3210 } 3211 3212 const delayMs = (parseInt(metaTag.getAttribute("content"), 10) || 0) * 1000; 3213 if (delayMs === 0) { 3214 return; 3215 } 3216 3217 const loginButton = document.querySelector("#LoginButton"); 3218 if (!loginButton) { 3219 return; 3220 } 3221 3222 this.schedule(delayMs); 3223 3224 const signForm = document.querySelector("#SignForm"); 3225 if (signForm) { 3226 signForm.addEventListener("show.bs.modal", this.cancel.bind(this)); 3227 } 3228 }, 3229 3230 schedule: function (delayMs) { 3231 this.cancel(); 3232 const self = this; 3233 this._timerId = window.setTimeout(function () { 3234 self._timerId = null; 3235 const loginButton = document.querySelector("#LoginButton"); 3236 if (loginButton) { 3237 loginButton.click(); 3238 } 3239 }, delayMs); 3240 }, 3241 3242 cancel: function () { 3243 if (this._timerId) { 3244 window.clearTimeout(this._timerId); 3245 this._timerId = null; 3246 } 3247 }, 3248}; 3249 3250kombi.googleOneTap = { 3251 waitForGoogle: function (callback, attempts) { 3252 let remaining = attempts || 50; 3253 3254 function tryInit() { 3255 if ( 3256 window.google && 3257 google.accounts && 3258 google.accounts.id 3259 ) { 3260 callback(); 3261 return; 3262 } 3263 3264 remaining -= 1; 3265 if (remaining <= 0) { 3266 return; 3267 } 3268 3269 window.setTimeout(tryInit, 100); 3270 } 3271 3272 tryInit(); 3273 }, 3274 3275 handleCredential: function (response, callbackUrl, nextUrl) { 3276 fetch(callbackUrl, { 3277 method: "POST", 3278 headers: { "Content-Type": "application/json" }, 3279 credentials: "include", 3280 body: JSON.stringify({ 3281 credential: response.credential, 3282 next_url: nextUrl, 3283 }), 3284 }) 3285 .then(function (res) { 3286 if (!res.ok) { 3287 return null; 3288 } 3289 return res.json(); 3290 }) 3291 .then(function (data) { 3292 if (data && data.redirect_url) { 3293 window.location.href = data.redirect_url; 3294 } 3295 }) 3296 .catch(function () {}); 3297 }, 3298 3299 init: function () { 3300 const configEl = document.getElementById("google-one-tap-config"); 3301 if (!configEl) { 3302 return; 3303 } 3304 3305 const clientId = configEl.dataset.clientId; 3306 const callbackUrl = configEl.dataset.callbackUrl; 3307 const nextUrl = configEl.dataset.nextUrl || "/"; 3308 3309 if (!clientId || !callbackUrl) { 3310 return; 3311 } 3312 3313 kombi.googleOneTap.waitForGoogle(function () { 3314 google.accounts.id.initialize({ 3315 client_id: clientId, 3316 context: "signin", 3317 use_fedcm_for_prompt: true, 3318 callback: function (response) { 3319 kombi.googleOneTap.handleCredential(response, callbackUrl, nextUrl); 3320 }, 3321 }); 3322 3323 google.accounts.id.prompt(); 3324 }); 3325 }, 3326}; 3327 3328document.addEventListener("DOMContentLoaded", function () { 3329 if (kombi.googleOneTap) { 3330 kombi.googleOneTap.init(); 3331 } 3332}); 3333; 3334kombi.searchBar = () => { 3335 document.querySelectorAll(".location-search").forEach((item) => { 3336 const label = item.querySelector(".location-label"); 3337 const value = item.querySelector(".location-value"); 3338 const origin_term = item.querySelector(".origin-term"); 3339 const input = item.querySelector(".location-input"); 3340 const options = item.querySelector(".location-options"); 3341 const terms_url = item.getAttribute("data-terms-url"); 3342 const search_type = item.getAttribute("data-search-type") || null; 3343 const clear_location = item.querySelector(".clear-location"); 3344 const toggle = item.querySelector('[data-bs-toggle="dropdown"]'); 3345 const menu = item.querySelector(".dropdown-menu"); 3346 const clearLabelReset = 3347 item.getAttribute("data-clear-label") || 3348 (label && label.getAttribute("title")) || 3349 ""; 3350 const groups = { 3351 "city": "Cidades", 3352 "state": "Estados", 3353 "country": "PaÃses", 3354 "neighborhood": "Bairros", 3355 "hotel": "Hotéis", 3356 "collection": "Coleções", 3357 "destination": "Destinos", 3358 "airport": "Aeroportos", 3359 } 3360 let waiting_set_interval = -1; 3361 3362 if (!input || !options) { 3363 return; 3364 } 3365 3366 const hintMinChars = "Digite 3 letras para começar a ver as opções"; 3367 3368 function setLabelText(text) { 3369 if (label) { 3370 label.textContent = text; 3371 } 3372 } 3373 3374 function setHasResults(hasResults) { 3375 menu?.classList.toggle("has-results", hasResults); 3376 } 3377
3378 function showMinCharsHint() { 3379 item.querySelectorAll(".search-text").forEach(function (element) { 3380 element.textContent = hintMinChars; 3381 }); 3382 setHasResults(false); 3383 options.innerHTML = ""; 3384 if (!item.querySelector(".search-text")) { 3385 const li = document.createElement("li"); 3386 li.className = "dropdown-item-text text-black px-3 py-2 small"; 3387 li.textContent = hintMinChars; 3388 options.append(li); 3389 } 3390 } 3391 3392 function getOppositeSelectedTerm() { 3393 const form = input.closest("form") || document; 3394 const selectedValue = search_type === "origin" 3395 ? form.querySelector( 3396 '.location-search[data-search-type="destination"] input[name="location"]' 3397 )?.value 3398 : form.querySelector( 3399 '.location-search[data-search-type="origin"] input[name="origin_airport_term"]' 3400 )?.value; 3401 const selectedParts = (selectedValue || "").split("|"); 3402 3403 if (!selectedParts[0] || !selectedParts[1]) { 3404 return ""; 3405 } 3406 3407 return `${selectedParts[0]}|${selectedParts[1]}`; 3408 } 3409 3410 input.addEventListener("keypress", function(event) { 3411 if (event.key === "Enter") { 3412 event.preventDefault(); 3413 } 3414 }); 3415 3416 if (toggle && toggle.contains(input) && toggle !== input) { 3417 input.addEventListener("click", (e) => { 3418 e.stopPropagation(); 3419 if (!menu?.classList.contains("show")) { 3420 bootstrap.Dropdown.getOrCreateInstance(toggle).show(); 3421 } 3422 }); 3423 3424 input.addEventListener("focus", () => { 3425 if (!menu?.classList.contains("show")) { 3426 bootstrap.Dropdown.getOrCreateInstance(toggle).show(); 3427 } 3428 }); 3429 } 3430 3431 item.addEventListener("shown.bs.dropdown", () => { 3432 if (typeof push_to_datalayer === "function") { 3433 const eventName = 3434 search_type === "origin" 3435 ? "search_interact_origin" 3436 : "search_interact_destination"; 3437 push_to_datalayer({ event: eventName }); 3438 } 3439 input.focus(); 3440 input.select(); 3441 if (!terms_url) { 3442 return; 3443 } 3444 if (input.value.length < 3) { 3445 showMinCharsHint(); 3446 } else { 3447 input.dispatchEvent(new Event("input", { bubbles: true })); 3448 } 3449 }); 3450 3451 clear_location?.addEventListener("click", (e) => { 3452 e.preventDefault(); 3453 e.stopPropagation(); 3454 clearTimeout(waiting_set_interval); 3455 value.value = ""; 3456 const originLabelClear = item.querySelector( 3457 'input[name="origin_airport_label"]' 3458 ); 3459 if (originLabelClear) { 3460 originLabelClear.value = ""; 3461 origin_term.value = "" 3462 } 3463 setLabelText(clearLabelReset); 3464 input.value = ""; 3465 if (terms_url) { 3466 showMinCharsHint(); 3467 } else { 3468 options.innerHTML = ""; 3469 setHasResults(false); 3470 } 3471 kombi.updateCalendar(null); 3472 }); 3473 3474 input.addEventListener("input", () => { 3475 if (!terms_url) return; 3476 if (input.value.length >= 3){ 3477 clearTimeout(waiting_set_interval); 3478 waiting_set_interval = setTimeout(() => { 3479 const fetchUrl = search_type 3480 ? `${ terms_url }?q=${encodeURIComponent(input.value)}&search_type=${search_type}&selected_search_term=${getOppositeSelectedTerm()}` 3481 : `${ terms_url }?q=${encodeURIComponent(input.value)}`; 3482 fetch(fetchUrl) 3483 .then(function(response) { 3484 return response.json(); 3485 }).then(function(data) { 3486 options.innerHTML = ""; 3487 3488 if (Object.keys(data).length === 0) { 3489 setHasResults(false); 3490 item.querySelectorAll(".search-text").forEach(function(element) { 3491 element.textContent = "Não encontramos um resultado para sua busca"; 3492 }); 3493 if (!item.querySelector(".search-text")) { 3494 const li = document.createElement("li"); 3495 li.className = "dropdown-item-text text-black px-3 py-2 small";
3496 li.textContent = "Não encontramos um resultado para sua busca"; 3497 options.append(li); 3498 } 3499 } else { 3500 setHasResults(true); 3501 item.querySelectorAll(".search-text").forEach(function(element) { 3502 element.textContent = "Resultados da busca"; 3503 }); 3504 3505 let isFirstGroup = true; 3506 for (const [key, val] of Object.entries(data)) { 3507 let group = document.createElement("li") 3508 let header = document.createElement("div"); 3509 header.className = isFirstGroup 3510 ? "dropdown-header bg-secondary-subtle text-black rounded-2" 3511 : "dropdown-header border-top bg-secondary-subtle text-black"; 3512 header.textContent = groups[key] || key; 3513 group.append(header); 3514 isFirstGroup = false; 3515 options.append(group) 3516 3517 val.map(function(option) { 3518 let li = document.createElement("li") 3519 let anchor = document.createElement("a"); 3520 anchor.href = "#"; 3521 anchor.className = "dropdown-item text-wrap"; 3522 anchor.textContent = option.label; 3523 li.append(anchor); 3524 li.addEventListener("click", (e) => { 3525 e.preventDefault(); 3526 kombi.updateCalendar(null) 3527 let storedValue = `${ option.type }|${ option.pk }|${ option.label }`; 3528 if ( 3529 search_type === "origin" && 3530 option.type === "airport" 3531 ) { 3532 const iataMatch = option.label.match( 3533 /\(([A-Z]{3})\)\s*$/ 3534 ); 3535 if (iataMatch) { 3536 storedValue = iataMatch[1]; 3537 } 3538 } 3539 value.value = storedValue; 3540 3541 if (search_type === "origin") { 3542 const originStoredValue = `${ option.type }|${ option.pk }|${ option.label }`; 3543 origin_term.value = originStoredValue; 3544 } 3545 3546 const originLabelInput = item.querySelector( 3547 'input[name="origin_airport_label"]' 3548 ); 3549 if (originLabelInput) { 3550 originLabelInput.value = option.label; 3551 } 3552 setLabelText(option.label); 3553 input.value = option.label; 3554 if (option.type === "hotel") { 3555 kombi.updateCalendar(option.pk); 3556 } 3557 const dd = bootstrap.Dropdown.getInstance(toggle); 3558 if (dd) { 3559 dd.hide(); 3560 } 3561 }) 3562 options.append(li) 3563 }) 3564 } 3565 } 3566 }); 3567 }, 300); 3568 } else { 3569 showMinCharsHint(); 3570 } 3571 }) 3572 }) 3573} 3574 3575kombi.showSearch = () => { 3576 const searchBar = document.querySelector(".d-sm-block .nav-search"); 3577 if (!searchBar) { 3578 window.location.href = "/#showSearch"; 3579 return 3580 } 3581 if (window.matchMedia("(min-width: 992px)").matches) { 3582 kombi.highlightElement(); 3583 } else { 3584 const modalElement = document.getElementById("SearchForm"); 3585 let modalInstance = bootstrap.Modal.getInstance(modalElement); 3586 if (!modalInstance) { 3587 modalInstance = new bootstrap.Modal(modalElement); 3588 } 3589 modalInstance.show(); 3590 } 3591}; 3592 3593kombi.checkSearchHash = () => { 3594 const hash = window.location.hash; 3595 3596 if (hash === "#showSearch") { 3597 history.replaceState(null, null, ' '); 3598 kombi.showSearch(); 3599 } 3600}; 3601; 3602/** 3603 * searchTabs - Alternância entre abas Hotel e Pacote na busca. 3604 * Sincroniza estado entre desktop e mobile. 3605 * Expõe getActiveTab(form) e isPackageActive() para outros módulos. 3606 * Dispara CustomEvent 'searchtab:change' em troca de aba e no init (estado inicial). 3607 */ 3608kombi.searchTabs = function () { 3609 const TAB = { 3610 HOTEL: 'hotel',
3611 PACKAGE: 'package' 3612 }; 3613 3614 const SELECTORS = { 3615 TAB_BUTTON: '[data-search-tab]', 3616 HOTEL_FIELDS: '.search-hotel-fields', 3617 PACKAGE_FIELDS: '.search-package-fields', 3618 SEARCH_SUBMIT_BTN: '[data-search-submit-btn]' 3619 }; 3620 3621 const CLASS = { 3622 HOTEL_ACTIVE: 'btn-primary', 3623 HOTEL_INACTIVE: 'btn-outline-primary bg-white border', 3624 PACKAGE_ACTIVE: 'btn-tab-package-active', 3625 PACKAGE_INACTIVE: 'btn-tab-package bg-white border text-package', 3626 HIDDEN: 'd-none' 3627 }; 3628 3629 let activeTab = TAB.HOTEL; 3630 3631 function getAllTabButtons() { 3632 return document.querySelectorAll(SELECTORS.TAB_BUTTON); 3633 } 3634 3635 function getTabValue(btn) { 3636 return btn.getAttribute('data-search-tab') || ''; 3637 } 3638 3639 function setActiveTab(value) { 3640 activeTab = value; 3641 } 3642 3643 function updateButtonStyles() { 3644 getAllTabButtons().forEach(function (btn) { 3645 const tabValue = getTabValue(btn); 3646 const isActive = tabValue === activeTab; 3647 3648 btn.classList.remove('btn-primary', 'btn-outline-primary', 'btn-tab-package', 'btn-tab-package-active', 'bg-white', 'border', 'text-primary-emphasis', 'text-package'); 3649 3650 if (tabValue === TAB.HOTEL) { 3651 btn.classList.add(...(isActive ? [CLASS.HOTEL_ACTIVE] : CLASS.HOTEL_INACTIVE.split(' '))); 3652 } else if (tabValue === TAB.PACKAGE) { 3653 if (isActive) { 3654 btn.classList.add(CLASS.PACKAGE_ACTIVE, 'btn-tab-package'); 3655 } else { 3656 btn.classList.add(...CLASS.PACKAGE_INACTIVE.split(' ')); 3657 } 3658 } 3659 }); 3660 } 3661 3662 function updateFieldsVisibility() { 3663 const hotelFields = document.querySelectorAll(SELECTORS.HOTEL_FIELDS); 3664 const packageFields = document.querySelectorAll(SELECTORS.PACKAGE_FIELDS); 3665 3666 hotelFields.forEach(function (el) { 3667 el.classList.toggle(CLASS.HIDDEN, activeTab !== TAB.HOTEL); 3668 }); 3669 3670 packageFields.forEach(function (el) { 3671 el.classList.toggle(CLASS.HIDDEN, activeTab !== TAB.PACKAGE); 3672 }); 3673 } 3674 3675 /** Disable inputs in the inactive tab so duplicate names (e.g. location) are not submitted. */ 3676 function syncTabInputsDisabled() { 3677 const controlSelector = 'input, select, textarea'; 3678 document.querySelectorAll('.SearchForm').forEach(function (form) { 3679 const hotelControls = form.querySelectorAll( 3680 `${SELECTORS.HOTEL_FIELDS} ${controlSelector}` 3681 ); 3682 const packageControls = form.querySelectorAll( 3683 `${SELECTORS.PACKAGE_FIELDS} ${controlSelector}` 3684 ); 3685 3686 const hotelActive = activeTab === TAB.HOTEL; 3687 hotelControls.forEach(function (el) { 3688 el.disabled = !hotelActive; 3689 }); 3690 packageControls.forEach(function (el) { 3691 el.disabled = hotelActive; 3692 }); 3693 }); 3694 } 3695 3696 function updateSearchButtonStyles() { 3697 document.querySelectorAll(SELECTORS.SEARCH_SUBMIT_BTN).forEach(function (btn) { 3698 btn.classList.toggle('btn-search-package', activeTab === TAB.PACKAGE); 3699 }); 3700 } 3701 3702 /** 3703 * Package vs hotel theme: toggles body + form classes for SCSS (hover, flatpickr, counters). 3704 * Flatpickr renders to the body, so the class must be on the document. 3705 */ 3706 function updateSearchTabThemeClass() { 3707 const isPackage = activeTab === TAB.PACKAGE; 3708 document.body.classList.toggle('search-tab-package-active', isPackage); 3709 document.querySelectorAll('.SearchForm').forEach(function (form) { 3710 form.classList.toggle('search-form--package', isPackage); 3711 }); 3712 } 3713 3714 function dispatchSearchTabChange() { 3715 document.dispatchEvent( 3716 new CustomEvent('searchtab:change', { 3717 detail: { 3718 activeTab: activeTab, 3719 isPackage: activeTab === TAB.PACKAGE 3720 } 3721 }) 3722 ); 3723 } 3724 3725 function switchTo(tabValue) { 3726 if (tabValue !== TAB.HOTEL && tabValue !== TAB.PACKAGE) return; 3727
3728 setActiveTab(tabValue); 3729 updateButtonStyles(); 3730 updateFieldsVisibility(); 3731 syncTabInputsDisabled(); 3732 updateSearchButtonStyles(); 3733 updateSearchTabThemeClass(); 3734 dispatchSearchTabChange(); 3735 } 3736 3737 function setupTabClickHandlers() { 3738 getAllTabButtons().forEach(function (btn) { 3739 btn.addEventListener('click', function () { 3740 const tabValue = getTabValue(btn); 3741 switchTo(tabValue); 3742 }); 3743 }); 3744 } 3745 3746 /** 3747 * Prefer the Package tab button state inside this form when tabs are rendered 3748 * together with the form (desktop). Fallback to the shared active tab for 3749 * contexts where the tab buttons live outside the form (mobile modal). 3750 */ 3751 function getActiveTab(form) { 3752 const packageBtn = form.querySelector('[data-search-tab="package"]'); 3753 if (packageBtn) { 3754 return packageBtn.classList.contains(CLASS.PACKAGE_ACTIVE) 3755 ? TAB.PACKAGE 3756 : TAB.HOTEL; 3757 } 3758 return activeTab; 3759 } 3760 3761 function isPackageActive() { 3762 return activeTab === TAB.PACKAGE; 3763 } 3764 3765 kombi.searchTabs.getActiveTab = getActiveTab; 3766 kombi.searchTabs.syncTabInputsDisabled = syncTabInputsDisabled; 3767 kombi.searchTabs.isPackageActive = isPackageActive; 3768 3769 function init() { 3770 activeTab = TAB.HOTEL; 3771 document.querySelectorAll('.search-tabs-container').forEach(function (container) { 3772 if (container.getAttribute('data-initial-tab') === TAB.PACKAGE) { 3773 activeTab = TAB.PACKAGE; 3774 } 3775 }); 3776 3777 setupTabClickHandlers(); 3778 updateButtonStyles(); 3779 updateFieldsVisibility(); 3780 syncTabInputsDisabled(); 3781 updateSearchButtonStyles(); 3782 updateSearchTabThemeClass(); 3783 dispatchSearchTabChange(); 3784 } 3785 3786 return init(); 3787}; 3788; 3789kombi.toggleReadMore = { 3790 init: function(context) { 3791 const searchContext = context || document; 3792 const mobileLimit = 70; 3793 const desktopLimit = 294; 3794 const limit = window.innerWidth < 576 ? mobileLimit : desktopLimit; 3795 3796 searchContext.querySelectorAll('.text-preview').forEach(function(textPreview) { 3797 const id = textPreview.getAttribute('data-id'); 3798 const fullText = textPreview.getAttribute('data-full-text') || textPreview.textContent.trim(); 3799 const toggleLink = document.querySelector('.toggle-read-more[data-id="' + id + '"]'); 3800 3801 if (!toggleLink) return; 3802 3803 if (fullText.length > limit && !textPreview.getAttribute('data-full-text')) { 3804 textPreview.setAttribute('data-full-text', fullText); 3805 textPreview.textContent = fullText.substr(0, limit) + '...'; 3806 toggleLink.classList.remove('d-none'); 3807 } else if (fullText.length <= limit) { 3808 toggleLink.classList.add('d-none'); 3809 } 3810 }); 3811 }, 3812 3813 click: function (toggleLink) { 3814 const mobileLimit = 70; 3815 const desktopLimit = 294; 3816 const limit = window.innerWidth < 576 ? mobileLimit : desktopLimit; 3817 3818 const id = toggleLink.getAttribute('data-id'); 3819 const wrapper = toggleLink.closest('.room-description-wrapper'); 3820 if (!wrapper) { 3821 console.error("Erro: container '.room-description-wrapper' não encontrado."); 3822 return; 3823 } 3824 3825 const textPreview = wrapper.querySelector('.preview-' + id); 3826 3827 if (!textPreview) return; 3828 3829 const fullText = textPreview.getAttribute('data-full-text'); 3830 const isLimited = textPreview.classList.contains('limited'); 3831 3832 if (isLimited) { 3833 textPreview.textContent = fullText; 3834 textPreview.classList.remove('limited'); 3835 toggleLink.textContent = 'Ver menos'; 3836 } else { 3837 textPreview.textContent = fullText.substr(0, limit) + (fullText.length > limit ? '...' : ''); 3838 textPreview.classList.add('limited'); 3839 toggleLink.textContent = 'Ver mais'; 3840 } 3841 } 3842}; 3843kombi.readMore = function (root) { 3844 const scope = root && root.querySelectorAll ? root : document; 3845 const defaultMobileVisibleLimit = 5; 3846 const defaultDesktopVisibleLimit = 7; 3847 3848 function getVisibleLimit(wrapper) { 3849 const mobileLimit = parseInt(wrapper.dataset.limitMobile, 10); 3850 const desktopLimit = parseInt(wrapper.dataset.limitDesktop, 10); 3851 const mobile = Number.isFinite(mobileLimit) ? mobileLimit : defaultMobileVisibleLimit; 3852 const desktop = Number.isFinite(desktopLimit) ? desktopLimit : defaultDesktopVisibleLimit; 3853 return window.innerWidth < 576 ? mobile : desktop; 3854 } 3855 3856 scope.querySelectorAll('.amenities-wrapper').forEach(function(wrapper) { 3857 const visibleLimit = getVisibleLimit(wrapper); 3858 const items = wrapper.querySelectorAll('.amenity-item:not(.see-more-wrapper)'); 3859 const btnWrappers = wrapper.querySelectorAll('.see-more-wrapper'); 3860 3861 if (items.length <= visibleLimit) {
3862 btnWrappers.forEach(function (w) { 3863 w.remove(); 3864 }); 3865 return; 3866 } 3867 3868 const btnWrapper = btnWrappers[0]; 3869 if (!btnWrapper) return; 3870 3871 const btn = btnWrapper.querySelector('.see-more-btn'); 3872 if (!btn) return; 3873 3874 if (btn.getAttribute('data-bs-toggle') === 'modal') { 3875 const mobileWrapper = wrapper.querySelector( 3876 '.see-more-wrapper:not(.see-more-wrapper--desktop)' 3877 ); 3878 const desktopWrapper = wrapper.querySelector('.see-more-wrapper--desktop'); 3879 const activeWrapper = 3880 window.innerWidth < 576 ? mobileWrapper : desktopWrapper || mobileWrapper; 3881 3882 btnWrappers.forEach(function (w) { 3883 if (w !== activeWrapper) { 3884 w.classList.add('d-none'); 3885 w.classList.remove('d-flex', 'd-sm-flex', 'd-sm-none'); 3886 } 3887 }); 3888 3889 if (!activeWrapper) return; 3890 3891 items.forEach((item, index) => { 3892 if (index >= visibleLimit) { 3893 item.classList.add('d-none'); 3894 } 3895 }); 3896 3897 const itemAfterLimit = items[visibleLimit]; 3898 if (itemAfterLimit) { 3899 itemAfterLimit.before(activeWrapper); 3900 } 3901 activeWrapper.classList.remove('d-none'); 3902 activeWrapper.classList.add('d-flex'); 3903 if (desktopWrapper && activeWrapper === desktopWrapper) { 3904 activeWrapper.classList.remove('d-sm-none'); 3905 } 3906 return; 3907 } 3908 3909 const itemAfterLimit = items[visibleLimit]; 3910 3911 if (itemAfterLimit) { 3912 itemAfterLimit.before(btnWrapper); 3913 } 3914 btnWrapper.classList.remove('d-none'); 3915 if (btn.dataset.readMoreInitialized) return; 3916 3917 btn.textContent = 'Ver mais'; 3918 btn.setAttribute('aria-expanded', 'false'); 3919 3920 items.forEach((item, index) => { 3921 if (index >= visibleLimit) { 3922 item.classList.add('d-none'); 3923 } 3924 }); 3925 3926 btn.addEventListener('click', function(event) { 3927 event.preventDefault(); 3928 const isExpanded = btn.getAttribute('aria-expanded') === 'true'; 3929 const currentLimit = getVisibleLimit(wrapper); 3930 3931 if (!isExpanded) { 3932 items.forEach(item => item.classList.remove('d-none')); 3933 wrapper.append(btnWrapper); 3934 btn.textContent = 'Ver menos'; 3935 btn.setAttribute('aria-expanded', 'true'); 3936 } else { 3937 items.forEach((item, index) => { 3938 if (index >= currentLimit) { 3939 item.classList.add('d-none'); 3940 } 3941 }); 3942 3943 const itemToPlaceBefore = items[currentLimit]; 3944 if (itemToPlaceBefore) { 3945 itemToPlaceBefore.before(btnWrapper); 3946 } 3947 btn.textContent = 'Ver mais'; 3948 btn.setAttribute('aria-expanded', 'false'); 3949 } 3950 }); 3951 3952 btn.dataset.readMoreInitialized = 'true'; 3953 }); 3954}; 3955; 3956 kombi.handleReservationSelection = function() { 3957 document.querySelectorAll('.select-reservation').forEach(function(button) { 3958 button.addEventListener('click', function(event) { 3959 let nextUrl = button.getAttribute('href'); 3960 localStorage.setItem('nextUrl', nextUrl); 3961 }); 3962 }); 3963}; 3964kombi.deltaPricing = { 3965 calculate: function (containerSelector) { 3966 const container = document.querySelector(containerSelector); 3967 3968 if (!container) { 3969 return; 3970 } 3971 3972 const items = container.querySelectorAll("[data-delta-total]"); 3973 3974 if (!items.length) { 3975 return; 3976 } 3977 3978 const highlightedItem = container.querySelector("[data-delta-highlighted]"); 3979 let baseTotal = null; 3980 3981 if (highlightedItem) { 3982 baseTotal = kombi.deltaPricing.parse( 3983 highlightedItem.getAttribute("data-delta-total") 3984 ); 3985 } 3986 3987 if (baseTotal === null) { 3988 const baselineAttr = container.getAttribute("data-delta-selection-baseline"); 3989 3990 if (baselineAttr) { 3991 baseTotal = kombi.deltaPricing.parse(baselineAttr); 3992 } 3993 } 3994 3995 if (baseTotal === null) { 3996 baseTotal = kombi.deltaPricing.parse( 3997 items[0].getAttribute("data-delta-total") 3998 ); 3999 } 4000 4001 if (baseTotal === null) { 4002 return; 4003 } 4004
4005 items.forEach(function (item) { 4006 const total = kombi.deltaPricing.parse(item.getAttribute("data-delta-total")); 4007 4008 if (total === null) { 4009 return; 4010 } 4011 4012 const delta = total - baseTotal; 4013 4014 item.querySelectorAll(".js-delta-price").forEach(function (element) { 4015 element.textContent = kombi.deltaPricing.formatDelta(delta); 4016 }); 4017 4018 item.querySelectorAll(".js-delta-total").forEach(function (element) { 4019 element.textContent = "Total R$ " + kombi.deltaPricing.format(total); 4020 }); 4021 }); 4022 }, 4023 4024 parse: function (value) { 4025 if (value === undefined || value === null) { 4026 return null; 4027 } 4028 4029 const raw = String(value).trim(); 4030 4031 if (raw === "" || raw.toLowerCase() === "nan") { 4032 return null; 4033 } 4034 4035 if (/^-?\d+(\.\d+)?$/.test(raw)) { 4036 const direct = parseFloat(raw); 4037 4038 return Number.isNaN(direct) ? null : direct; 4039 } 4040 4041 if (raw.includes(",")) { 4042 const normalized = raw.replace(/\./g, "").replace(",", "."); 4043 const n = parseFloat(normalized); 4044 4045 return Number.isNaN(n) ? null : n; 4046 } 4047 4048 const fallback = parseFloat(raw.replace(/\s/g, "")); 4049 4050 return Number.isNaN(fallback) ? null : fallback; 4051 }, 4052 4053 format: function (value) { 4054 return new Intl.NumberFormat("pt-BR", { 4055 minimumFractionDigits: 2, 4056 maximumFractionDigits: 2, 4057 }).format(value); 4058 }, 4059 4060 formatDelta: function (value) { 4061 const prefix = value < 0 ? "- R$ " : "+ R$ "; 4062 4063 if (Math.abs(value) === 0) { 4064 return prefix + "0"; 4065 } 4066 4067 return prefix + kombi.deltaPricing.format(Math.abs(value)); 4068 }, 4069}; 4070; 4071kombi.flight = { 4072 bindDeltaPricing: function () { 4073 if (kombi.flight._deltaPricingBound) { 4074 return; 4075 } 4076 4077 const recalcFlightDeltaPricing = function (event) { 4078 const targetId = event.detail.target && event.detail.target.id; 4079 4080 if (targetId === 'flight_results_list') { 4081 kombi.flight.calculateDeltaPricing(); 4082 } 4083 }; 4084 4085 document.body.addEventListener('htmx:afterSwap', recalcFlightDeltaPricing); 4086 document.body.addEventListener('htmx:afterSettle', recalcFlightDeltaPricing); 4087 4088 kombi.flight._deltaPricingBound = true; 4089 }, 4090 syncPackageCheckoutVisibility: function (formEl) { 4091 if (!formEl || !formEl.getAttribute || formEl.getAttribute('data-package-checkout-card') !== '1') { 4092 return; 4093 } 4094 4095 const cartDepartureFlightId = formEl.getAttribute('data-package-cart-flight-departure-id'); 4096 const cartArrivalFlightId = formEl.getAttribute('data-package-cart-flight-arrival-id'); 4097 const checkoutEl = formEl.querySelector('.js-package-direct-checkout'); 4098 const resubmitEl = formEl.querySelector('.js-package-resubmit-flight'); 4099 4100 if (!cartDepartureFlightId || !cartArrivalFlightId || !checkoutEl || !resubmitEl) { 4101 return; 4102 } 4103 4104 const departureInput = formEl.querySelector('input[name="flightCardDepartureFlight"]:checked'); 4105 const arrivalInput = formEl.querySelector('input[name="flightCardArrivalFlight"]:checked'); 4106 4107 if (!departureInput || !arrivalInput) { 4108 return; 4109 } 4110 4111 const matches = 4112 String(departureInput.value) === String(cartDepartureFlightId) && 4113 String(arrivalInput.value) === String(cartArrivalFlightId); 4114 4115 if (matches) { 4116 checkoutEl.classList.remove('d-none'); 4117 resubmitEl.classList.add('d-none'); 4118 } else { 4119 checkoutEl.classList.add('d-none'); 4120 resubmitEl.classList.remove('d-none'); 4121 } 4122 }, 4123 initPackageCheckoutVisibility: function () { 4124 document.querySelectorAll('form[data-package-checkout-card="1"]').forEach(function (form) {
4125 kombi.flight.syncPackageCheckoutVisibility(form); 4126 }); 4127 }, 4128 bindPackageCheckoutVisibility: function () { 4129 if (kombi.flight._packageCheckoutBound) { 4130 return; 4131 } 4132 4133 const resyncFlightResultsList = function (event) { 4134 const targetId = event.detail.target && event.detail.target.id; 4135 4136 if (targetId === 'flight_results_list') { 4137 kombi.flight.initPackageCheckoutVisibility(); 4138 } 4139 }; 4140 4141 document.body.addEventListener('change', function (event) { 4142 const el = event.target; 4143 4144 if (!el.classList || !el.classList.contains('flightCheck')) { 4145 return; 4146 } 4147 4148 const pkgForm = el.closest('form[data-package-checkout-card="1"]'); 4149 4150 if (pkgForm) { 4151 kombi.flight.syncPackageCheckoutVisibility(pkgForm); 4152 } 4153 }); 4154 4155 document.body.addEventListener('htmx:afterSwap', resyncFlightResultsList); 4156 document.body.addEventListener('htmx:afterSettle', resyncFlightResultsList); 4157 4158 kombi.flight._packageCheckoutBound = true; 4159 }, 4160 calculateDeltaPricing: function () { 4161 if (!kombi.deltaPricing) { 4162 return; 4163 } 4164 4165 kombi.deltaPricing.calculate('#flight_results_list'); 4166 }, 4167 changeDateMobile: function () { 4168 let buttons = document.querySelectorAll('.changeDateButton'); 4169 if (window.innerWidth < 768) { 4170 buttons.forEach(function (button) { 4171 button.setAttribute('data-bs-toggle', 'modal'); 4172 button.setAttribute('data-bs-target', '#SearchForm'); 4173 }); 4174 } else { 4175 buttons.forEach(function (button) { 4176 button.removeAttribute('data-bs-toggle'); 4177 button.removeAttribute('data-bs-target'); 4178 }); 4179 } 4180 }, 4181 changeDateDesktop: function (room_id, element) { 4182 if (!room_id || !element) { 4183 return; 4184 } 4185 4186 document.querySelectorAll('.modal').forEach(modal => { 4187 let modalInstance = bootstrap.Modal.getInstance(modal); 4188 if (modalInstance) { 4189 modalInstance.hide(); 4190 } 4191 }); 4192 4193 setTimeout(() => { 4194 window.scrollTo({ top: 0, behavior: 'smooth' }); 4195 }, 200); 4196 4197 setTimeout(() => { 4198 let calendarElement = null; 4199 if (window.innerWidth < 768) { 4200 calendarElement = document.getElementById('search-calendar-mobile'); 4201 } else { 4202 calendarElement = document.getElementById('search-calendar-desk'); 4203 } 4204 4205 if (calendarElement) { 4206 if (kombi.flatpickrInstances && kombi.flatpickrInstances.length > 0) { 4207 for (let i = 0; i < kombi.flatpickrInstances.length; i++) { 4208 const fp = kombi.flatpickrInstances[i]; 4209 if (fp.input && calendarElement.contains(fp.input)) { 4210 fp.open(); 4211 return; 4212 } 4213 } 4214 } 4215 } 4216 }, 600); 4217 }, 4218 moreFlights: function (event) { 4219 let moreFlightsButton = event.target; 4220 let moreFlightsDivs = moreFlightsButton.closest('.flights-card').querySelectorAll('.more-flights-flightCard'); 4221 moreFlightsDivs.forEach(function(div) { 4222 if (div.classList.contains('show')) { 4223 moreFlightsButton.innerHTML = 'Ver mais opções de horários<i class="fa-solid fa-chevron-down ms-1"></i>'; 4224 div.classList.remove('show'); 4225 } else { 4226 moreFlightsButton.innerHTML = 'Ver menos opções de horários<i class="fa-solid fa-chevron-up ms-1"></i>'; 4227 div.classList.add('show'); 4228 } 4229 }); 4230 }, 4231 selectionButton: function (button, multi_offer_form_id) { 4232 var allSelectionButtons = document.querySelectorAll('.spinner-disabled'); 4233 allSelectionButtons.forEach(function(btn) { 4234 btn.disabled = true; 4235 }); 4236 4237 var buttonText = button.querySelector('.buttonText'); 4238 var buttonSpinner = button.querySelector('.buttonSpinner'); 4239 buttonText.classList.add('d-none'); 4240 buttonSpinner.classList.remove('d-none'); 4241 4242 var formObj = document.getElementById(multi_offer_form_id); 4243 var formData = new FormData(formObj); 4244 var selectionString = '&selectedFlight='; 4245 selectionString += formData.get('multi_offer_id') + '.'; 4246 selectionString += formData.get('flightCardDepartureFlight') + '.'; 4247 selectionString += formData.get('flightCardArrivalFlight'); 4248 4249 formObj.action = formObj.action + selectionString;
4250 if (formObj.hasAttribute('hx-post')) { 4251 formObj.setAttribute('hx-post', formObj.action); 4252 } 4253 if (typeof formObj.requestSubmit === 'function' && formObj.hasAttribute('hx-post')) { 4254 formObj.requestSubmit(); 4255 } else { 4256 formObj.submit(); 4257 } 4258 }, 4259 /** 4260 * Starts the rotating copy inside the async flight loader. 4261 * Reusable across flight results page and package hotel poller. 4262 * 4263 * Keeps a single interval; it reads the "live" DOM each tick so HTMX swaps don't break it. 4264 * 4265 * @param {ParentNode} root Element/document to search within 4266 */ 4267 startLoaderTextRotation: function (root) { 4268 const isGlobalRotation = !root || root === document; 4269 4270 // If an interval exists but the "mode" changed (document vs scoped), 4271 // restart so we always query the correct live DOM. 4272 if ( 4273 kombi.flight._loaderTextIntervalId != null && 4274 kombi.flight._loaderTextIsGlobalRotation === isGlobalRotation 4275 ) { 4276 return; 4277 } 4278 4279 kombi.flight.stopLoaderTextRotation(); 4280 kombi.flight._loaderTextIsGlobalRotation = isGlobalRotation; 4281 4282 let currentTextIndex = 0; 4283 4284 const getContainer = function () { 4285 // Prefer the live DOM node: HTMX can recreate the loader with the same id. 4286 const live = document.getElementById("loader-container"); 4287 if (live) return live; 4288 4289 if (root && root.querySelector) { 4290 return root.querySelector("#loader-container"); 4291 } 4292 4293 return null; 4294 }; 4295 4296 const updateLoaderTextRotation = function () { 4297 const container = getContainer(); 4298 if (!container) { 4299 kombi.flight.stopLoaderTextRotation(); 4300 return; 4301 } 4302 4303 const texts = container.querySelectorAll(".loader-text"); 4304 if (!texts || texts.length < 2) {
4305 // With 0/1 texts, nothing to rotate. 4306 return; 4307 } 4308 4309 const current = currentTextIndex % texts.length; 4310 texts.forEach((textEl) => textEl.classList.add("d-none")); 4311 texts[current].classList.remove("d-none"); 4312 currentTextIndex = current + 1; 4313 }; 4314 4315 const initialContainer = getContainer(); 4316 const rotationIntervalMs = 4317 parseFloat( 4318 initialContainer?.getAttribute("data-flight-loader-text-interval") || "5", 4319 10, 4320 ) * 1000; 4321 4322 updateLoaderTextRotation(); 4323 kombi.flight._loaderTextIntervalId = window.setInterval( 4324 updateLoaderTextRotation, 4325 Math.max(800, rotationIntervalMs), 4326 ); 4327 }, 4328 // Backwards compatible alias (used in templates / other scripts) 4329 initLoaderTextRotation: function (root) { 4330 kombi.flight.startLoaderTextRotation(root); 4331 }, 4332 stopLoaderTextRotation: function () { 4333 if (kombi.flight._loaderTextIntervalId != null) { 4334 window.clearInterval(kombi.flight._loaderTextIntervalId); 4335 kombi.flight._loaderTextIntervalId = null; 4336 } 4337 kombi.flight._loaderTextIsGlobalRotation = null; 4338 }, 4339}; 4340function toggleAccordionButtonStyle(button, active) { 4341 if (!button) return; 4342 4343 const classes = ['border-bottom-0', 'shadow-none', 'rounded-bottom-0']; 4344 classes.forEach(cls => button.classList.toggle(cls, active)); 4345} 4346 4347kombi.accordion = function() { 4348 const accordionsCollapse = document.querySelectorAll('.accordion-item.custom-plain .accordion-collapse'); 4349 4350 accordionsCollapse.forEach(accordion => { 4351 accordion.addEventListener('show.bs.collapse', () => { 4352 const button = accordion.closest('.accordion-item')?.querySelector('.accordion-button'); 4353 toggleAccordionButtonStyle(button, true); 4354 4355 /*TODO: ajustar quando tiver o aereo. 4356 accordionsCollapse.forEach(otherAccordion => { 4357 if (otherAccordion !== accordion) { 4358 const otherInstance = bootstrap.Collapse.getInstance(otherAccordion); 4359 if (otherInstance) { 4360 otherInstance.hide(); 4361 const otherButton = otherAccordion.closest('.accordion-item')?.querySelector('.accordion-button'); 4362 toggleAccordionButtonStyle(otherButton, false); 4363 } 4364 } 4365 });*/ 4366 }); 4367 4368 accordion.addEventListener('hide.bs.collapse', () => { 4369 const button = accordion.closest('.accordion-item')?.querySelector('.accordion-button'); 4370 toggleAccordionButtonStyle(button, false); 4371 }); 4372 }); 4373}; 4374 4375kombi.accordionScroll = function(type, roomLoopCounter) { 4376 const accordion = document.querySelector("#accordionResulOccupation") 4377 const accordionItem = accordion.querySelector("#accordionItem" + roomLoopCounter); 4378 if (type == "accordionButton") { 4379 accordion.scrollIntoView({ 4380 behavior: 'smooth', 4381 block: 'start' 4382 }); 4383 } else { 4384 accordionItem.scrollIntoView({ 4385 behavior: 'smooth', 4386 block: 'start' 4387 }); 4388 } 4389}; 4390kombi.spinner = function(button) { 4391 if (!button) { 4392 return; 4393 } 4394 4395 const currentHeight = button.offsetHeight; 4396 button.style.minHeight = currentHeight + 'px'; 4397 4398 button.classList.add('disabled'); 4399 var buttonText = button.querySelector('.buttonText'); 4400 if (buttonText) { 4401 buttonText.classList.add('d-none'); 4402 } 4403 var spinner = button.querySelector('.buttonSpinner'); 4404 if (spinner) { 4405 spinner.classList.remove('d-none'); 4406 } 4407} 4408 4409kombi.resetButton = function(button) { 4410 if (!button) return; 4411 4412 button.classList.remove('disabled'); 4413 4414 const buttonText = button.querySelector('.buttonText'); 4415 if (buttonText) { 4416 buttonText.classList.remove('d-none'); 4417 } 4418 4419 const spinner = button.querySelector('.buttonSpinner'); 4420 if (spinner) { 4421 spinner.classList.add('d-none'); 4422 } 4423 4424 button.style.minHeight = ''; 4425}; 4426kombi.search = function () { 4427 const MAX = { 4428 ROOMS: 5, 4429 ADULTS: 10, 4430 CHILDREN: 5,
4431 PACKAGE_GUESTS: 9 4432 }; 4433 const MIN = { 4434 ROOMS: 1, 4435 ADULTS: 1, 4436 CHILDREN: 0 4437 }; 4438 4439 const SELECTORS = { 4440 GUEST_FORM_CONTAINER: '.room-container', 4441 ROOM_SELECTION_CARD: '.room', 4442 SEARCH_WIDGET_WRAPPER: '.guest-selector-wrapper, .guest-selector-dropdown, .guest-selector-dropdown-desktop, .guests', 4443 TRAVELER_SUMMARY: '#search-travelers-desk, .block-guests.guests', 4444 ADULT_INPUT: 'input[name="adults"]', 4445 CHILD_INPUT: 'input[name="children"]', 4446 AGE_OF_CHILDREN: 'input[name="age_of_children"]', 4447 CHILD_TOOLTIP: '.children-tooltip[data-bs-toggle="tooltip"]', 4448 ROOM_COUNT_INPUT: 'input.number_of_rooms', 4449 GUESTS_COUNT_INPUT: 'input.number_of_guests', 4450 AGE_SELECT_CONTAINER: '.age-control', 4451 AGE_TEMPLATE_SELECTOR: '.html-age-control', 4452 AGE_SELECT: '.age_text', 4453 TOAST_ROOM_LIMIT: '#room-limit-toast', 4454 BTN_ADD_ROOM: '.btn-add-room', 4455 BTN_REMOVE_LAST_ROOM: '.btn-minus-room', 4456 BTN_REMOVE_ROOM: '.btn-remove-room', 4457 BTN_INCREMENT: '.btn-plus', 4458 BTN_DECREMENT: '.btn-minus', 4459 QUANTITY_SELECTOR_GROUP: '.counter-group', 4460 ROOM_LABEL: '.label-title', 4461 TRAVELER_TOTAL_INPUT: 'input[name="number_of_guests"]', 4462 ROOM_COUNTER_ELEMENTS: '.room-display-count, input.number_of_room', 4463 SEARCH_SUMMARY_INPUT: 'input.resume', 4464 SEARCH_RESULTS_LABEL: '#search-travelers-desk .resume:not(input)', 4465 COLLAPSE_GUEST_SELECTOR: '#collapseGuests', 4466 DIVIDER: 'hr', 4467 PACKAGE_GUEST_LIMIT_MSG: '#package-guest-limit-message' 4468 }; 4469 4470 const CLASS = { 4471 HIDDEN: 'd-none' 4472 }; 4473 4474 const LIMITS = { 4475 adults: { 4476 min: MIN.ADULTS, 4477 max: MAX.ADULTS 4478 }, 4479 children: { 4480 min: MIN.CHILDREN, 4481 max: MAX.CHILDREN 4482 }, 4483 number_of_rooms: { 4484 min: MIN.ROOMS, 4485 max: MAX.ROOMS 4486 } 4487 }; 4488 4489 let cachedChildAgeSelectHTML = ''; 4490 4491 function getChildAgeSelectTemplate() { 4492 if (cachedChildAgeSelectHTML) return cachedChildAgeSelectHTML; 4493 4494 const template = document.querySelector(SELECTORS.AGE_TEMPLATE_SELECTOR); 4495 if (template?.innerHTML.trim()) { 4496 return (cachedChildAgeSelectHTML = template.innerHTML.trim()); 4497 } 4498 4499 const select = document.querySelector(`${SELECTORS.ROOM_SELECTION_CARD} ${SELECTORS.AGE_SELECT_CONTAINER} select`); 4500 return (cachedChildAgeSelectHTML = select?.outerHTML || ''); 4501 } 4502 4503 function getChildCount(roomCard) { 4504 const childInput = roomCard.querySelector(SELECTORS.CHILD_INPUT); 4505 return childInput ? parseInt(childInput.value || 0) : 0; 4506 } 4507 4508 function getAgeContainer(roomCard) { 4509 return roomCard.querySelector(SELECTORS.AGE_SELECT_CONTAINER); 4510 } 4511 4512 function getAges(roomCard) { 4513 const agesString = roomCard.getAttribute('data-children-ages'); 4514 if (!agesString) return []; 4515 try { 4516 return JSON.parse(agesString); 4517 } catch { 4518 return []; 4519 } 4520 } 4521 4522 function createAgeSelect(age) { 4523 const wrapper = document.createElement('div'); 4524 wrapper.innerHTML = getChildAgeSelectTemplate(); 4525 4526 const select = wrapper.querySelector('select'); 4527 4528 if (!select) return null; 4529 4530 Array.from(select.options).forEach(opt => { 4531 opt.defaultSelected = (parseInt(opt.value) === (age !== undefined ? parseInt(age) : 6)); 4532 }); 4533 4534 return select; 4535 } 4536 4537 function removeExtraAgeSelectors(ageContainer, countToKeep) { 4538 Array.from(ageContainer.children).slice(countToKeep).forEach(el => el.remove()); 4539 if (countToKeep < 1) { 4540 ageContainer.closest(SELECTORS.AGE_SELECT).classList.add('d-none') 4541 ageContainer.closest(SELECTORS.ROOM_SELECTION_CARD).setAttribute('data-children-ages', '[]') 4542 } 4543 } 4544 4545 function syncChildAgeSelectors(roomCard) { 4546 const childCount = getChildCount(roomCard); 4547 const ageContainer = getAgeContainer(roomCard); 4548 if (!ageContainer) return; 4549 if (childCount > 0) { 4550 ageContainer.closest(SELECTORS.AGE_SELECT).classList.remove('d-none') 4551 } 4552 const current = ageContainer.children.length; 4553 const ages = getAges(roomCard); 4554 4555 if (childCount < current) { 4556 removeExtraAgeSelectors(ageContainer, childCount); 4557 } else { 4558 const fragment = document.createDocumentFragment(); 4559 for (let i = current; i < childCount; i++) { 4560 const select = createAgeSelect(ages[i]); 4561 if (select) fragment.appendChild(select); 4562 } 4563 ageContainer.appendChild(fragment); 4564 } 4565 } 4566 4567 function formatTravelerSummary(roomCount, guestCount) { 4568 const roomText = Number(roomCount) === 1 ? 'quarto' : 'quartos'; 4569 const guestText = Number(guestCount) === 1 ? 'hóspede' : 'hóspedes'; 4570 return `${String(roomCount).padStart(2, '0')} ${roomText} para ${String(guestCount).padStart(2, '0')} ${guestText}`; 4571 } 4572 4573 function applyTravelerSummary(roomCount, guestCount, searchWidgetWrapper) { 4574 const summaryText = formatTravelerSummary(roomCount, guestCount); 4575 4576 searchWidgetWrapper.querySelectorAll(SELECTORS.TRAVELER_TOTAL_INPUT).forEach((totalGuestsInput)
4576=> { 4577 totalGuestsInput.value = guestCount; 4578 }); 4579 4580 searchWidgetWrapper.querySelectorAll(SELECTORS.ROOM_COUNTER_ELEMENTS).forEach((roomCountElement) => { 4581 const val = String(roomCount).padStart(2, '0'); 4582 roomCountElement.tagName === 'INPUT' ? (roomCountElement.value = val) : (roomCountElement.textContent = val); 4583 }); 4584 4585 const hiddenRoomInput = searchWidgetWrapper.querySelector(SELECTORS.ROOM_COUNT_INPUT); 4586 if (hiddenRoomInput) hiddenRoomInput.value = String(roomCount).padStart(2, '0'); 4587 4588 const guestsBlock = searchWidgetWrapper.closest('.guests') || searchWidgetWrapper; 4589 guestsBlock.querySelectorAll(SELECTORS.SEARCH_SUMMARY_INPUT).forEach((summaryInput) => { 4590 summaryInput.value = summaryText; 4591 }); 4592 4593 guestsBlock.querySelectorAll('.resume:not(input)').forEach((resumeLabelElement) => { 4594 resumeLabelElement.textContent = summaryText; 4595 }); 4596 } 4597 4598 function updateTravelerSummary(guestFormContainer, searchWidgetWrapper) { 4599 const rooms = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD); 4600 let totalAdults = 0, 4601 totalChildren = 0; 4602 4603 rooms.forEach(room => { 4604 const adults = parseInt(room.querySelector(SELECTORS.ADULT_INPUT)?.value || 0, 10); 4605 const children = parseInt(room.querySelector(SELECTORS.CHILD_INPUT)?.value || 0, 10); 4606 4607 totalAdults += adults; 4608 totalChildren += children; 4609 }); 4610 4611 const totalGuests = totalAdults + totalChildren; 4612 const totalRooms = rooms.length; 4613 4614 applyTravelerSummary(totalRooms, totalGuests, searchWidgetWrapper); 4615 } 4616 4617 function getTotalGuestsInForm(guestFormContainer) { 4618 const rooms = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD); 4619 let total = 0; 4620 rooms.forEach((room) => { 4621 total += parseInt(room.querySelector(SELECTORS.ADULT_INPUT)?.value || 0, 10); 4622 total += parseInt(room.querySelector(SELECTORS.CHILD_INPUT)?.value || 0, 10); 4623 }); 4624 return total; 4625 } 4626 4627 function serializeGuestOccupation(guestFormContainer) { 4628 const serializedGuestRooms = []; 4629 guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach((room) => { 4630 serializedGuestRooms.push({ 4631 adults: parseInt(room.querySelector(SELECTORS.ADULT_INPUT)?.value || 0, 10), 4632 children: parseInt(room.querySelector(SELECTORS.CHILD_INPUT)?.value || 0, 10), 4633 ages: getAges(room) || [] 4634 }); 4635 }); 4636 return serializedGuestRooms; 4637 } 4638 4639 function rebindGuestChildTooltips(guestFormContainer) { 4640 guestFormContainer.querySelectorAll(SELECTORS.CHILD_TOOLTIP).forEach((tooltipElement) => { 4641 const existingTooltip = bootstrap.Tooltip.getInstance(tooltipElement); 4642 if (existingTooltip) existingTooltip.dispose(); 4643 if (typeof bootstrap !== "undefined" && bootstrap.Tooltip) { 4644 new bootstrap.Tooltip(tooltipElement); 4645 } 4646 }); 4647 } 4648 4649 function syncGuestFormInnerHTMLToPeer(sourceGuestForm, targetGuestForm, targetSearchWidgetWrapper) { 4650 if (sourceGuestForm === targetGuestForm) return; 4651 targetGuestForm.innerHTML = sourceGuestForm.innerHTML; 4652 rebindGuestChildTooltips(targetGuestForm); 4653 updateRoomSearchState(targetGuestForm, targetSearchWidgetWrapper); 4654 const sourceSearchWidgetWrapper = sourceGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 4655 if (sourceSearchWidgetWrapper) { 4656 applyPackageGuestCap(sourceGuestForm, sourceSearchWidgetWrapper); 4657 } 4658 } 4659 4660 function syncPeerGuestFormFrom(sourceGuestForm) { 4661 const allGuestFormContainers = document.querySelectorAll(SELECTORS.GUEST_FORM_CONTAINER); 4662 if (allGuestFormContainers.length < 2) return; 4663 const peerGuestForm = sourceGuestForm === allGuestFormContainers[0] 4664 ? allGuestFormContainers[1] 4665 : allGuestFormContainers[0]; 4666 const peerSearchWidgetWrapper = peerGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 4667 if (peerSearchWidgetWrapper) { 4668 syncGuestFormInnerHTMLToPeer(sourceGuestForm, peerGuestForm, peerSearchWidgetWrapper); 4669 } 4670 } 4671 4672 function markPackageGuestsDirtyIfNeeded() { 4673 if ( 4674 kombi.searchTabs && 4675 typeof kombi.searchTabs.isPackageActive === "function" && 4676 kombi.searchTabs.isPackageActive() 4677 ) { 4678 kombi._packageGuestsDirty = true; 4679 } 4680 } 4681 4682 function resetGuestFormToPackageDefault(guestFormContainer, searchWidgetWrapper) { 4683 const roomCardElements = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD); 4684 for (let i = roomCardElements.length - 1; i > 0; i--) { 4685 roomCardElements[i].remove(); 4686 } 4687 const firstRoomCard = guestFormContainer.querySelector(SELECTORS.ROOM_SELECTION_CARD); 4688 if (firstRoomCard) { 4689 firstRoomCard.setAttribute("data-children-ages", "[]"); 4690 firstRoomCard.querySelector(SELECTORS.ADULT_INPUT).value = "02"; 4691 firstRoomCard.querySelector(SELECTORS.CHILD_INPUT).value = "00"; 4692 const ageSelectContainer = firstRoomCard.querySelector(SELECTORS.AGE_SELECT_CONTAINER); 4693 if (ageSelectContainer) ageSelectContainer.innerHTML = ""; 4694 firstRoomCard.querySelector(SELECTORS.AGE_SELECT).classList.add(CLASS.HIDDEN); 4695 syncChildAgeSelectors(firstRoomCard); 4696 } 4697 rebindGuestChildTooltips(guestFormContainer); 4698 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 4699 } 4700 4701 function applySerializedGuestOccupation(guestFormContainer, searchWidgetWrapper, occupation) { 4702 if (!Array.isArray(occupation) || !occupation.length) return; 4703 const existingRoomCards = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD); 4704 for (let i = existingRoomCards.length - 1; i >= 0; i--) { 4705 if (i >= occupation.length) { 4706 existingRoomCards[i].remove(); 4707 } 4708 } 4709 for (let i = 0; i < occupation.length; i++) { 4710 let roomCard = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD)[i]; 4711 if (!roomCard) { 4712 const lastRoomCard = guestFormContainer.querySelector( 4713 `${SELECTORS.ROOM_SELECTION_CARD}:last-child` 4714 ); 4715 if (!lastRoomCard) return; 4716 const clonedRoomCard = lastRoomCard.cloneNode(true); 4717 const tooltipInClone = clonedRoomCard.querySelector(SELECTORS.CHILD_TOOLTIP); 4718 if (tooltipInClone) { 4719 const oldTooltip = bootstrap.Tooltip.getInstance(tooltipInClone); 4720 if (oldTooltip) oldTooltip.dispose(); 4721 new bootstrap.Tooltip(tooltipInClone); 4722 } 4723 guestFormContainer.appendChild(clonedRoomCard); 4724 initializeRoomCardFields(clonedRoomCard, false); 4725 roomCard = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD)[i]; 4726 } 4727 const roomOccupation = occupation[i]; 4728 const childAges = roomOccupation.ages || []; 4729 roomCard.setAttribute("data-children-ages", JSON.stringify(childAges)); 4730 roomCard.querySelector(SELECTORS.ADULT_INPUT).value =
4730String(roomOccupation.adults).padStart(2, "0"); 4731 roomCard.querySelector(SELECTORS.CHILD_INPUT).value = String(roomOccupation.children).padStart(2, "0"); 4732 syncChildAgeSelectors(roomCard); 4733 } 4734 rebindGuestChildTooltips(guestFormContainer); 4735 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 4736 } 4737 4738 /** 4739 * Package tab: cap total guests at MAX.PACKAGE_GUESTS; toggle message and disable + / add-room when capped. 4740 */ 4741 function applyPackageGuestCap(guestFormContainer, searchWidgetWrapper) { 4742 const msg = searchWidgetWrapper.querySelector(SELECTORS.PACKAGE_GUEST_LIMIT_MSG); 4743 const isPackage = 4744 kombi.searchTabs && 4745 typeof kombi.searchTabs.isPackageActive === "function" && 4746 kombi.searchTabs.isPackageActive(); 4747 4748 if (!isPackage) { 4749 if (msg) msg.classList.add(CLASS.HIDDEN); 4750 guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach((room) => { 4751 room.querySelectorAll(SELECTORS.QUANTITY_SELECTOR_GROUP).forEach((group) => { 4752 refreshQuantitySelectorGroup(group, searchWidgetWrapper); 4753 }); 4754 }); 4755 const btnPlusRoom = searchWidgetWrapper.querySelector(SELECTORS.BTN_ADD_ROOM); 4756 const roomCount = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).length; 4757 if (btnPlusRoom) btnPlusRoom.disabled = roomCount >= MAX.ROOMS; 4758 return; 4759 } 4760 4761 const totalGuests = getTotalGuestsInForm(guestFormContainer); 4762 const capped = totalGuests >= MAX.PACKAGE_GUESTS; 4763 4764 if (msg) msg.classList.toggle(CLASS.HIDDEN, !capped); 4765 4766 const btnPlusRoom = searchWidgetWrapper.querySelector(SELECTORS.BTN_ADD_ROOM); 4767 const roomCount = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).length; 4768 const blockAddRoom = 4769 totalGuests + MIN.ADULTS > MAX.PACKAGE_GUESTS; 4770 if (btnPlusRoom) { 4771 btnPlusRoom.disabled = capped || roomCount >= MAX.ROOMS || blockAddRoom; 4772 } 4773 4774 guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach((room) => { 4775 room.querySelectorAll(SELECTORS.QUANTITY_SELECTOR_GROUP).forEach((group) => { 4776 const input = group.querySelector("input"); 4777 if (!input || (input.name !== "adults" && input.name !== "children")) { 4778 return; 4779 } 4780 refreshQuantitySelectorGroup(group, searchWidgetWrapper); 4781 if (capped) { 4782 const inc = group.querySelector(SELECTORS.BTN_INCREMENT); 4783 if (inc) inc.disabled = true; 4784 } 4785 }); 4786 }); 4787 } 4788 4789 function initializeRoomCardFields(roomCard, clearFields=false) { 4790 const adults = clearFields ? 2 : MIN.ADULTS; 4791 roomCard.querySelector(SELECTORS.ADULT_INPUT).value = String(adults).padStart(2, '0'); 4792 roomCard.querySelector(SELECTORS.CHILD_INPUT).value = String(MIN.CHILDREN).padStart(2, '0'); 4793 roomCard.querySelector(SELECTORS.AGE_SELECT_CONTAINER).innerHTML = ''; 4794 if (MIN.CHILDREN < 1) { 4795 roomCard.querySelector(SELECTORS.AGE_SELECT).classList.add('d-none') 4796 } 4797 } 4798 4799 function updateQuantityButtonState(group, fieldName, value) { 4800 const limits = LIMITS[fieldName]; 4801 if (!limits) return; 4802 4803 group.querySelector(SELECTORS.BTN_DECREMENT).disabled = value <= limits.min; 4804 group.querySelector(SELECTORS.BTN_INCREMENT).disabled = value >= limits.max; 4805 } 4806 4807 function refreshQuantitySelectorGroup(group, searchWidgetWrapper) { 4808 const input = group.querySelector('input'); 4809 if (!input) return; 4810 4811 let value = parseInt(input.value || 0); 4812 const fieldName = input.name; 4813 4814 if (fieldName === 'number_of_rooms' && searchWidgetWrapper) { 4815 value = parseInt(searchWidgetWrapper.querySelector(SELECTORS.ROOM_COUNT_INPUT)?.value || 0); 4816 } 4817 4818 updateQuantityButtonState(group, fieldName, value); 4819 } 4820 4821 function updateRoomSearchState(guestFormContainer, searchWidgetWrapper) { 4822 guestFormContainer.querySelectorAll(SELECTORS.DIVIDER).forEac
4822h(hr => hr.remove()); 4823 4824 const roomCards = Array.from(guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD)); 4825 roomCards.forEach((roomCard, index) => { 4826 const roomNumber = index + 1; 4827 roomCard.dataset.room = roomNumber; 4828 roomCard.querySelector(SELECTORS.ROOM_LABEL).textContent = `Quarto ${String(roomNumber).padStart(2, '0')}`; 4829 roomCard.querySelector(SELECTORS.BTN_REMOVE_ROOM).classList.toggle(CLASS.HIDDEN, index === 0); 4830 4831 if (index > 0) { 4832 const divider = document.createElement('hr'); 4833 divider.className = 'my-3'; 4834 guestFormContainer.insertBefore(divider, roomCard); 4835 } 4836 4837 syncChildAgeSelectors(roomCard); 4838 }); 4839 4840 const btnMinusRoom = searchWidgetWrapper.querySelector(SELECTORS.BTN_REMOVE_LAST_ROOM); 4841 if (btnMinusRoom) { 4842 const roomCount = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).length; 4843 btnMinusRoom.disabled = roomCount <= MIN.ROOMS; 4844 } 4845 4846 const btnPlusRoom = searchWidgetWrapper.querySelector(SELECTORS.BTN_ADD_ROOM); 4847 if (btnPlusRoom) { 4848 const roomCount = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).length; 4849 btnPlusRoom.disabled = roomCount >= MAX.ROOMS; 4850 } 4851 4852 const roomLimitMsg = searchWidgetWrapper.querySelector('#room-limit-message'); 4853 if (roomLimitMsg) { 4854 const roomCount = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).length; 4855 roomLimitMsg.classList.toggle('d-none', roomCount < MAX.ROOMS); 4856 } 4857 4858 updateTravelerSummary(guestFormContainer, searchWidgetWrapper); 4859 refreshQuantitySelectorGroup(searchWidgetWrapper.querySelector(SELECTORS.QUANTITY_SELECTOR_GROUP), searchWidgetWrapper); 4860 roomCards.forEach(room => room.querySelectorAll(SELECTORS.QUANTITY_SELECTOR_GROUP).forEach(group => refreshQuantitySelectorGroup(group))); 4861 applyPackageGuestCap(guestFormContainer, searchWidgetWrapper); 4862 } 4863 4864 function setupRoomSelectionForm() { 4865 document.querySelectorAll(SELECTORS.GUEST_FORM_CONTAINER).forEach(guestFormContainer => { 4866 const searchWidgetWrapper = guestFormContainer.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 4867 if (!searchWidgetWrapper) return; 4868 4869 setupAddRoomButton(guestFormContainer, searchWidgetWrapper); 4870 setupRemoveRoomButton(guestFormContainer, searchWidgetWrapper); 4871 setupRoomCardInteractions(guestFormContainer, searchWidgetWrapper); 4872 4873 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 4874 }); 4875 } 4876 4877 function setupAddRoomButton(guestFormContainer, searchWidgetWrapper) { 4878 const btn = searchWidgetWrapper.querySelector(SELECTORS.BTN_ADD_ROOM); 4879 if (!btn) return; 4880 4881 btn.addEventListener('click', () => { 4882 const currentRooms = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).length; 4883 if (currentRooms >= MAX.ROOMS) return showRoomLimitWarningToast(searchWidgetWrapper); 4884 if ( 4885 kombi.searchTabs && 4886 typeof kombi.searchTabs.isPackageActive === "function" && 4887 kombi.searchTabs.isPackageActive() && 4888 getTotalGuestsInForm(guestFormContainer) + MIN.ADULTS > MAX.PACKAGE_GUESTS 4889 ) { 4890 return; 4891 } 4892 4893 const lastRoomCard = guestFormContainer.querySelector(`${SELECTORS.ROOM_SELECTION_CARD}:last-child`); 4894 if (!lastRoomCard) return; 4895 4896 const clonedRoomCard = lastRoomCard.cloneNode(true); 4897 4898 if (clonedRoomCard) { 4899 clonedRoomCard.setAttribute('data-children-ages', '[]') 4900 const tooltipEl = clonedRoomCard.querySelector(SELECTORS.CHILD_TOOLTIP); 4901 if (tooltipEl) { 4902 const existingTooltip = bootstrap.Tooltip.getInstance(tooltipEl); 4903 if (existingTooltip) existingTooltip.dispose(); 4904 new bootstrap.Tooltip(tooltipEl); 4905 } 4906 4907 } 4908 4909 initializeRoomCardFields(clonedRoomCard); 4910 guestFormContainer.appendChild(clonedRoomCard); 4911 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 4912 kombi._lastGuestFormSource = guestFormContainer; 4913 markPackageGuestsDirtyIfNeeded(); 4914 syncPeerGuestFormFrom(guestFormContainer); 4915 }); 4916 } 4917 4918 function setupRemoveRoomButton(guestFormContainer, searchWidgetWrapper) { 4919 const btn = searchWidgetWrapper.querySelector(SELECTORS.BTN_REMOVE_LAST_ROOM); 4920 if (!btn) return; 4921 4922 btn.addEventListener('click', () => { 4923 const rooms = guestFormContainer.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD); 4924 if (rooms.length > MIN.ROOMS) { 4925 rooms[rooms.length - 1].remove(); 4926 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 4927 kombi._lastGuestFormSource = guestFormContainer; 4928 markPackageGuestsDirtyIfNeeded(); 4929 syncPeerGuestFormFrom(guestFormContainer); 4930 } 4931 }); 4932 } 4933 4934 function setupRoomCardInteractions(guestFormContainer, searchWidgetWrapper) { 4935 guestFormContainer.addEventListener('click', e => { 4936 const target = e.target; 4937 const roomCard = target.closest(SELECTORS.ROOM_SELECTION_CARD); 4938 const group = target.closest(SELECTORS.QUANTITY_SELECTOR_GROUP); 4939 4940 if (!roomCard) return; 4941 4942 if (target.closest(SELECTORS.BTN_REMOVE_ROOM)) { 4943 roomCard.remove(); 4944 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 4945 kombi._lastGuestFormSource = guestFormContainer; 4946 markPackageGuestsDirtyIfNeeded(); 4947 syncPeerGuestFormFrom(guestFormContainer); 4948 return; 4949 } 4950 4951 if (!group) return; 4952 const input = group.querySelector('input'); 4953 if (!input) return; 4954
4955 let val = parseInt(input.value || 0); 4956 const limits = LIMITS[input.name]; 4957 if (!limits) return; 4958 4959 if (target.closest(SELECTORS.BTN_INCREMENT) && val < limits.max) { 4960 if ( 4961 kombi.searchTabs && 4962 typeof kombi.searchTabs.isPackageActive === "function" && 4963 kombi.searchTabs.isPackageActive() && 4964 getTotalGuestsInForm(guestFormContainer) >= MAX.PACKAGE_GUESTS 4965 ) { 4966 return; 4967 } 4968 val++; 4969 } 4970 if (target.closest(SELECTORS.BTN_DECREMENT) && val > limits.min) val--; 4971 4972 input.value = String(val).padStart(2, '0'); 4973 4974 if (input.name === 'children') { 4975 syncChildAgeSelectors(roomCard); 4976 } 4977 4978 updateTravelerSummary(guestFormContainer, searchWidgetWrapper); 4979 refreshQuantitySelectorGroup(group); 4980 applyPackageGuestCap(guestFormContainer, searchWidgetWrapper); 4981 kombi._lastGuestFormSource = guestFormContainer; 4982 markPackageGuestsDirtyIfNeeded(); 4983 syncPeerGuestFormFrom(guestFormContainer); 4984 }); 4985 } 4986 4987 function setupGuestSelectorCollapseListener() { 4988 const collapseElement = document.querySelector(SELECTORS.COLLAPSE_GUEST_SELECTOR); 4989 if (collapseElement) { 4990 collapseElement.addEventListener('shown.bs.collapse', () => { 4991 document.querySelectorAll(SELECTORS.GUEST_FORM_CONTAINER).forEach(container => { 4992 const wrapper = container.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 4993 if (wrapper) updateRoomSearchState(container, wrapper); 4994 }); 4995 }); 4996 } 4997 } 4998 4999 function initSearchWidget() { 5000 const roomInput = document.querySelector(SELECTORS.ROOM_COUNT_INPUT) 5001 const guestInput = document.querySelector(SELECTORS.GUESTS_COUNT_INPUT) 5002 const roomCount = roomInput ? roomInput.value : 0 5003 const guestCount = guestInput ? guestInput.value : 0 5004 const travelersSummary = document.querySelectorAll(SELECTORS.TRAVELER_SUMMARY) 5005 getChildAgeSelectTemplate(); 5006 setupRoomSelectionForm(); 5007 setupGuestSelectorCollapseListener(); 5008 const firstGuestFormContainer = document.querySelector(SELECTORS.GUEST_FORM_CONTAINER); 5009 if (firstGuestFormContainer) { 5010 kombi._lastGuestFormSource = firstGuestFormContainer; 5011 } 5012 if (kombi._packageGuestsDirty == null) { 5013 kombi._packageGuestsDirty = false; 5014 } 5015 if (kombi._hotelGuestSnapshot == null) { 5016 kombi._hotelGuestSnapshot = null; 5017 } 5018 if (kombi.searchTabs && typeof kombi.searchTabs.isPackageActive === "function") { 5019 kombi._lastTabWasPackage = kombi.searchTabs.isPackageActive(); 5020 } else { 5021 kombi._lastTabWasPackage = false; 5022 } 5023 const guestFormContainersOnInit = document.querySelectorAll(SELECTORS.GUEST_FORM_CONTAINER); 5024 if (guestFormContainersOnInit.length >= 2) { 5025 const desktopGuestForm = guestFormContainersOnInit[0]; 5026 const mobileGuestForm = guestFormContainersOnInit[1]; 5027 const desktopSearchWidgetWrapper = desktopGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 5028 const mobileSearchWidgetWrapper = mobileGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 5029 if (desktopSearchWidgetWrapper && mobileSearchWidgetWrapper) { 5030 syncGuestFormInnerHTMLToPeer( 5031 desktopGuestForm, 5032 mobileGuestForm, 5033 mobileSearchWidgetWrapper 5034 ); 5035 } 5036 } 5037 if (!kombi._searchTabChangeGuestCapBound) { 5038 kombi._searchTabChangeGuestCapBound = true; 5039 document.addEventListener("searchtab:change", function (e) { 5040 const isPackageTabActive = Boolean(e.detail && e.detail.isPackage); 5041 const previousTabWasPackage = kombi._lastTabWasPackage; 5042 const guestFormContainers = document.querySelectorAll(SELECTORS.GUEST_FORM_CONTAINER); 5043 if (guestFormContainers.length < 2) {
5044 guestFormContainers.forEach(function (guestFormContainer) { 5045 const searchWidgetWrapper = guestFormContainer.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 5046 if (searchWidgetWrapper) { 5047 updateRoomSearchState(guestFormContainer, searchWidgetWrapper); 5048 } 5049 }); 5050 } else { 5051 const desktopGuestForm = guestFormContainers[0]; 5052 const mobileGuestForm = guestFormContainers[1]; 5053 const desktopSearchWidgetWrapper = desktopGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 5054 const mobileSearchWidgetWrapper = mobileGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 5055 const lastEditedGuestForm = kombi._lastGuestFormSource; 5056 if ( 5057 lastEditedGuestForm && 5058 (lastEditedGuestForm === desktopGuestForm || lastEditedGuestForm === mobileGuestForm) && 5059 desktopSearchWidgetWrapper && 5060 mobileSearchWidgetWrapper 5061 ) { 5062 if (lastEditedGuestForm === desktopGuestForm) { 5063 syncGuestFormInnerHTMLToPeer( 5064 desktopGuestForm, 5065 mobileGuestForm, 5066 mobileSearchWidgetWrapper 5067 ); 5068 } else { 5069 syncGuestFormInnerHTMLToPeer( 5070 mobileGuestForm, 5071 desktopGuestForm, 5072 desktopSearchWidgetWrapper 5073 ); 5074 } 5075 } else if (desktopSearchWidgetWrapper && mobileSearchWidgetWrapper) { 5076 syncGuestFormInnerHTMLToPeer( 5077 desktopGuestForm, 5078 mobileGuestForm, 5079 mobileSearchWidgetWrapper 5080 ); 5081 } 5082 if (previousTabWasPackage === false && isPackageTabActive) { 5083 if (getTotalGuestsInForm(desktopGuestForm) > MAX.PACKAGE_GUESTS) { 5084 kombi._hotelGuestSnapshot = serializeGuestOccupation(desktopGuestForm); 5085 kombi._packageGuestsDirty = false; 5086 resetGuestFormToPackageDefault(desktopGuestForm, desktopSearchWidgetWrapper); 5087 resetGuestFormToPackageDefault(mobileGuestForm, mobileSearchWidgetWrapper); 5088 } 5089 } else if (previousTabWasPackage === true && !isPackageTabActive) { 5090 if ( 5091 !kombi._packageGuestsDirty && 5092 kombi._hotelGuestSnapshot && 5093 desktopSearchWidgetWrapper && 5094 mobileSearchWidgetWrapper 5095 ) { 5096 applySerializedGuestOccupation( 5097 desktopGuestForm, 5098 desktopSearchWidgetWrapper, 5099 kombi._hotelGuestSnapshot 5100 ); 5101 applySerializedGuestOccupation( 5102 mobileGuestForm, 5103 mobileSearchWidgetWrapper, 5104 kombi._hotelGuestSnapshot 5105 ); 5106 kombi._hotelGuestSnapshot = null; 5107 } 5108 } 5109 } 5110 kombi._lastTabWasPackage = isPackageTabActive; 5111 }); 5112 }
5113 travelersSummary.forEach(function(travelerSummary) { 5114 applyTravelerSummary(roomCount, guestCount, travelerSummary); 5115 }) 5116 } 5117 5118 function updateAgeInputForRoom(room) { 5119 const selects = room.querySelectorAll('select.age_of_children_select'); 5120 const ageInput = room.querySelector(SELECTORS.AGE_OF_CHILDREN); 5121 5122 if (!ageInput) return; 5123 5124 ageInput.value = Array.from(selects) 5125 .map(sel => sel.value) 5126 .filter(Boolean) 5127 .join(','); 5128 } 5129 5130 let dismissOpenValidationTooltip = null; 5131 5132 function showTooltip(element, title) { 5133 if (!element) return; 5134 5135 dismissOpenValidationTooltip?.(); 5136 5137 const existingTooltip = bootstrap.Tooltip.getInstance(element); 5138 if (existingTooltip) existingTooltip.dispose(); 5139 5140 const tooltip = new bootstrap.Tooltip(element, { 5141 title, 5142 html: true, 5143 trigger: "manual", 5144 placement: "top", 5145 }); 5146 tooltip.show(); 5147 5148 const hideAfterClick = () => { 5149 dismissOpenValidationTooltip = null; 5150 const instance = bootstrap.Tooltip.getInstance(element); 5151 if (instance) instance.dispose(); 5152 }; 5153 5154 dismissOpenValidationTooltip = () => { 5155 document.removeEventListener("click", hideAfterClick); 5156 dismissOpenValidationTooltip = null; 5157 const instance = bootstrap.Tooltip.getInstance(element); 5158 if (instance) instance.dispose(); 5159 }; 5160 5161 document.addEventListener("click", hideAfterClick, { once: true }); 5162 } 5163 5164 function showDateRequiredTooltip(form) { 5165 const inputEl = form.querySelector(".calendar"); 5166 showTooltip(inputEl, ` 5167 <div class="fs-6"> 5168 <strong>Preencha as datas</strong><br> 5169 Para uma melhor experiência, preencha as datas e realize a busca. 5170 </div> 5171 `); 5172 } 5173 5174 function showDestinationRequiredTooltip(form) { 5175 const destEl = 5176 form.querySelector("#search-destination-package-desk") || 5177 form.querySelectorAll(".search-package-fields .location-search")[1]; 5178 showTooltip(destEl, ` 5179 <div class="fs-6"> 5180 <strong>Preencha o destino</strong><br> 5181 Para uma melhor experiência, preencha o destino e realize a busca. 5182 </div> 5183 `); 5184 } 5185 5186 function showOriginRequiredTooltip(form) { 5187 const originEl = 5188 form.querySelector("#search-origin-package-desk") || 5189 form.querySelectorAll(".search-package-fields .location-search")[0]; 5190 showTooltip(originEl, ` 5191 <div class="fs-6"> 5192 <strong>Preencha a origem</strong><br> 5193 Para uma melhor experiência, preencha a origem e realize a busca. 5194 </div> 5195 `); 5196 } 5197 5198 function isDateFilled(form) { 5199 const stayStart = form.querySelector('input[name="stay_start"]'); 5200 return stayStart && stayStart.value && stayStart.value.trim() !== ""; 5201 } 5202 5203 function isPackageOriginFilled(form) { 5204 const originInput = form.querySelector('.search-package-fields input[name="origin_airport"]'); 5205 return Boolean(originInput && originInput.value && originInput.value.trim() !== ""); 5206 } 5207 5208 function isPackageDestinationFilled(form) { 5209 const destInput = form.querySelector('.search-package-fields input[name="location"]'); 5210 return Boolean(destInput && destInput.value && destInput.value.trim() !== ""); 5211 } 5212 5213 document.querySelectorAll(".SearchForm").forEach(form => { 5214 form.addEventListener("submit", (e) => { 5215 if (typeof kombi.searchTabs.syncTabInputsDisabled === "function") { 5216 kombi.searchTabs.syncTabInputsDisabled(); 5217 } 5218 const activeTab = kombi.searchTabs.getActiveTab(form); 5219 const roomsInput = form.querySelector("input.room-display-count"); 5220 const rooms = roomsInput ? parseInt(roomsInput.value, 10) : 1; 5221 5222 const adultsInput = form.querySelectorAll("input[name='adults']"); 5223 const childrenInput = form.querySelectorAll("input[name='children']"); 5224 const adults = Array.from(adultsInput).reduce((sum, input) => { 5225 return sum + (parseInt(input.value) || 0); 5226 }, 0); 5227 const children = Array.from(childrenInput).reduce((sum, input) => { 5228 return sum + (parseInt(input.value) || 0); 5229 }, 0); 5230 5231 const dateFilled = isDateFilled(form); 5232 const isHotelDefaultConfig = rooms === 1 && adults === 2 && children === 0; 5233 5234 if (activeTab === "package") { 5235 const originFilled = isPackageOriginFilled(form); 5236 const destinationFilled = isPackageDestinationFilled(form); 5237 5238 if (!originFilled) { 5239 showOriginRequiredTooltip(form); 5240 e.preventDefault(); 5241 document.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEac
5241h(updateAgeInputForRoom); 5242 return; 5243 } 5244 5245 if (!destinationFilled) { 5246 showDestinationRequiredTooltip(form); 5247 e.preventDefault(); 5248 document.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach(updateAgeInputForRoom); 5249 return; 5250 } 5251 5252 if (!dateFilled) { 5253 showDateRequiredTooltip(form); 5254 e.preventDefault(); 5255 document.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach(updateAgeInputForRoom); 5256 return; 5257 } 5258 } else if (!isHotelDefaultConfig && !dateFilled) { 5259 showDateRequiredTooltip(form); 5260 e.preventDefault(); 5261 document.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach(updateAgeInputForRoom); 5262 return; 5263 } 5264 5265 const submitBtn = e.submitter; 5266 if (submitBtn) kombi.spinner(submitBtn); 5267 document.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD).forEach(updateAgeInputForRoom); 5268 }); 5269 }); 5270 5271 function setupDropdownCloseIcon({ parentId, toggleSelector, closeIconSelector }) { 5272 const parent = document.getElementById(parentId); 5273 if (!parent) return; 5274 5275 const dropdownToggle = parent.querySelector(toggleSelector); 5276 const closeIcon = parent.querySelector(closeIconSelector); 5277 if (!dropdownToggle || !closeIcon) return; 5278 5279 parent.addEventListener("shown.bs.dropdown", () => { 5280 closeIcon.classList.remove("d-none"); 5281 }); 5282 5283 parent.addEventListener("hidden.bs.dropdown", () => { 5284 closeIcon.classList.add("d-none"); 5285 }); 5286 } 5287 5288 setupDropdownCloseIcon({ 5289 parentId: "search-travelers-desk", 5290 toggleSelector: '[data-bs-toggle="dropdown"]', 5291 closeIconSelector: ".closeIcon", 5292 }); 5293 5294 setupDropdownCloseIcon({ 5295 parentId: "search-location-desk", 5296 toggleSelector: '[data-bs-toggle="dropdown"]', 5297 closeIconSelector: ".clear-location", 5298 }); 5299 5300 setupDropdownCloseIcon({ 5301 parentId: "search-origin-package-desk", 5302 toggleSelector: '[data-bs-toggle="dropdown"]', 5303 closeIconSelector: ".clear-location", 5304 }); 5305 5306 setupDropdownCloseIcon({ 5307 parentId: "search-destination-package-desk", 5308 toggleSelector: '[data-bs-toggle="dropdown"]', 5309 closeIconSelector: ".clear-location", 5310 }); 5311 5312 kombi.closeNavSearchDropdowns = function (root) { 5313 if (!root || typeof bootstrap === "undefined" || !bootstrap.Dropdown) return; 5314 root.querySelectorAll('[data-bs-toggle="dropdown"]').forEach((toggle) => { 5315 const dropdownInstance = bootstrap.Dropdown.getInstance(toggle); 5316 if (dropdownInstance) dropdownInstance.hide(); 5317 }); 5318 }; 5319 5320 kombi.closeNavSearchFlatpickrs = function (root) { 5321 if (!root || !kombi.flatpickrInstances) return; 5322 kombi.flatpickrInstances.forEach((flatpickrInstance) => { 5323 if (flatpickrInstance.isOpen && root.contains(flatpickrInstance.element)) { 5324 flatpickrInstance.close(); 5325 } 5326 }); 5327 }; 5328 5329 document 5330 .querySelectorAll(".nav-search, #SearchForm .pattern-form") 5331 .forEach((root) => { 5332 root.addEventListener("show.bs.dropdown", (e) => { 5333 const openingToggle = e.target?.closest?.( 5334 '[data-bs-toggle="dropdown"]' 5335 ); 5336 if (!openingToggle || !root.contains(openingToggle)) { 5337 return; 5338 } 5339 root.querySelectorAll('[data-bs-toggle="dropdown"]').forEach((toggle) => { 5340 if (toggle !== openingToggle) { 5341 const dropdownInstance = bootstrap.Dropdown.getInstance(toggle); 5342 if (dropdownInstance) dropdownInstance.hide(); 5343 } 5344 }); 5345 kombi.closeNavSearchFlatpickrs(root); 5346 }); 5347 }); 5348 5349 document.querySelectorAll(".guests .closeIcon").forEach((icon) => { 5350 icon.addEventListener("click", () => { 5351 const allGuestFormContainers = document.querySelectorAll(SELECTORS.GUEST_FORM_CONTAINER); 5352 if (!allGuestFormContainers.length) return; 5353 const desktopGuestForm = allGuestFormContainers[0]; 5354 const desktopSearchWidgetWrapper = desktopGuestForm.closest(SELECTORS.SEARCH_WIDGET_WRAPPER); 5355 if (!desktopSearchWidgetWrapper) return; 5356 const roomCardElements = Array.from( 5357 desktopGuestForm.querySelectorAll(SELECTORS.ROOM_SELECTION_CARD) 5358 ); 5359 if (roomCardElements.length) { 5360 initializeRoomCardFields(roomCardElements[0], true); 5361 for (let i = 1; i < roomCardElements.length; i++) { 5362 roomCardElements[i].remove(); 5363 } 5364 } 5365 updateRoomSearchState(desktopGuestForm, desktopSearchWidgetWrapper); 5366 updateTravelerSummary(desktopGuestForm, desktopSearchWidgetWrapper); 5367 applyPackageGuestCap(desktopGuestForm, desktopSearchWidgetWrapper); 5368 kombi._lastGuestFormSource = desktopGuestForm; 5369 if (allGuestFormContainers.length > 1) { 5370 syncPeerGuestFormFrom(desktopGuestForm); 5371 } 5372 markPackageGuestsDirtyIfNeeded(); 5373 }); 5374 }); 5375 5376 return initSearchWidget(); 5377}; 5378 5379; 5380kombi.password = function (event) { 5381 const password = event.currentTarget.closest('.pattern-form-floating').querySelector('input'); 5382 const type = password.getAttribute('type') === 'password' ? 'text' : 'password'; 5383 password.setAttribute('type', type); 5384 const eyeIcon = event.currentTarget.querySelector('.eye-icon'); 5385 eyeIcon.classList.toggle('fa-eye-slash'); 5386 eyeIcon.classList.toggle('fa-eye'); 5387}; 5388kombi.copyTransferArea = function (event) { 5389 const copyElement = event.target.closest('.copy-div'); 5390 const copyText = copyElement.querySelector('.copy-text'); 5391 const transferArea = document.createElement("textarea"); 5392 5393 transferArea.value = copyText.innerText; 5394 document.body.appendChild(transferArea); 5395 transferArea.select(); 5396 5397 navigator.clipboard.writeText(transferArea.value) 5398 .then(() => { 5399 if (event.target.matches('button.copy-button') || event.target.closest('button.copy-button')) { 5400 const btn = event.target.closest('button.copy-button'); 5401 if (btn) { 5402 const btnText = btn.querySelector('span'); 5403 btnText.textContent = 'Copiado!'; 5404 btn.classList.remove('btn-primary'); 5405 btn.classList.add('btn-success', 'text-white'); 5406 } 5407 } 5408 }) 5409 .catch(function (err) { 5410 console.error('Erro ao copiar para a área de transferência:', err); 5411 }) 5412 .finally(function () { 5413 document.body.removeChild(transferArea); 5414 const copiedElement = copyElement.querySelector('.copy-check') || document.querySelector('.copy-check'); 5415 5416 if (copiedElement) { 5417 copiedElement.classList.remove("d-none", "fade-out"); 5418 copiedElement.classList.add("fade-in"); 5419 5420 setTimeout(function () { 5421 copiedElement.classList.remove("fade-in"); 5422 copiedElement.classList.add("fade-out"); 5423 5424 setTimeout(function () { 5425 copiedElement.classList.add("d-none"); 5426 }, 500); 5427 }, 2000); 5428 } 5429 }); 5430}; 5431; 5432kombi.fullNameValidator = function (full_name) { 5433 /** 5434 * Validation ensures that the string: 5435 * - Has at least two non-space characters before a whitespace 5436 * - Has at least two non-space characters after the whitespace 5437 * - The rest of the string contains only letters (uppercase or lowercase) and spaces 5438 */ 5439 const regex = /^[A-Za-z]{2,}\s[A-Za-z]{2,}(?:\s[A-Za-z]*)*$/; 5440 return regex.test(full_name); 5441};; 5442(function () { 5443 "use strict"; 5444 5445 if (window.hotelSortListenerAdded) return; 5446 window.hotelSortListenerAdded = true; 5447 5448 const HIDDEN_CLASS = "d-none"; 5449 const SORT_FORM_SELECTOR = ".hotel-sort-form"; 5450 const SORT_BUTTON_SELECTOR = ".sort-option-btn";
5451 const POLL_TARGET_IDS = ["search-poll-div", "collection-poll-div"]; 5452 5453 function updateSortBadges() { 5454 const hasSort = !!document.querySelector(SORT_BUTTON_SELECTOR + '[aria-pressed="true"]'); 5455 document.querySelectorAll(".sort-active-badge").forEach(function (badge) { 5456 badge.classList.toggle(HIDDEN_CLASS, !hasSort); 5457 }); 5458 } 5459 5460 function pushSortSelectionDatalayer(button) { 5461 if (!button || typeof push_to_datalayer !== "function") return; 5462 5463 const sortKey = button.getAttribute("data-sort-value"); 5464 if (!sortKey) return; 5465 5466 push_to_datalayer({ 5467 event: "hotel_sort_list", 5468 sort_type: [sortKey], 5469 }); 5470 } 5471 5472 function onSortButtonClick(event) { 5473 const button = event.target.closest(SORT_BUTTON_SELECTOR); 5474 if (!button || button.disabled) return; 5475 5476 const form = button.closest(SORT_FORM_SELECTOR); 5477 if (!form) return; 5478 5479 pushSortSelectionDatalayer(button); 5480 5481 form.querySelectorAll(SORT_BUTTON_SELECTOR).forEach(function (option) { 5482 option.removeAttribute("data-sort-loading"); 5483 }); 5484 button.setAttribute("data-sort-loading", "true"); 5485 } 5486 5487 function isPollTarget(target) { 5488 return !!(target && target.id && POLL_TARGET_IDS.indexOf(target.id) !== -1); 5489 } 5490 5491 function onHtmxBeforeSwap(event) { 5492 if (!isPollTarget(event.detail && event.detail.target)) return; 5493 if (kombi.closeOffcanvas) kombi.closeOffcanvas("offcanvasSort"); 5494 if (kombi.closeModal) kombi.closeModal("sortModal"); 5495 } 5496 5497 function cleanupOrphanBackdrops() { 5498 document.body.classList.remove("modal-open"); 5499 document.body.style.overflow = ""; 5500 document.body.style.paddingRight = ""; 5501 document.querySelectorAll(".modal-backdrop, .offcanvas-backdrop").forEach(function (backdrop) { 5502 backdrop.remove(); 5503 }); 5504 } 5505 5506 function onHtmxAfterSettle(event) { 5507 if (isPollTarget(event.detail && event.detail.target)) { 5508 cleanupOrphanBackdrops(); 5509 } 5510 updateSortBadges(); 5511 } 5512 5513 function isSortFormRequest(event) { 5514 const elt = event.detail && event.detail.elt; 5515 return !!(elt && elt.closest && elt.closest(SORT_FORM_SELECTOR)); 5516 } 5517 5518 function resetSortLoading(form) { 5519 const scope = form && form.querySelectorAll ? form : document; 5520 scope.querySelectorAll(SORT_BUTTON_SELECTOR).forEach(function (option) { 5521 option.removeAttribute("data-sort-loading"); 5522 }); 5523 } 5524 5525 function onHtmxAfterRequest(event) { 5526 if (!isSortFormRequest(event)) return; 5527 const successful = event.detail && event.detail.successful; 5528 if (!successful) { 5529 resetSortLoading(event.detail && event.detail.elt && event.detail.elt.closest(SORT_FORM_SELECTOR)); 5530 } 5531 } 5532 5533 document.addEventListener("click", onSortButtonClick); 5534 document.addEventListener("htmx:beforeSwap", onHtmxBeforeSwap); 5535 document.addEventListener("htmx:afterSettle", onHtmxAfterSettle); 5536 document.addEventListener("htmx:afterRequest", onHtmxAfterRequest); 5537 5538 updateSortBadges(); 5539})(); 5540 5541kombi.hotel = function (root) { 5542 const scope = root || document; 5543 scope.querySelectorAll(".toggleRoomsLink").forEach(function (toggleLink) { 5544 if (kombi.lifecycle.isInitialized(toggleLink, "room-toggle")) { 5545 return; 5546 } 5547 const targetId = toggleLink.getAttribute("href"); 5548 if (!targetId) return; 5549 5550 const collapseElement = 5551 scope.querySelector(targetId) || document.querySelector(targetId); 5552 if (!collapseElement) return; 5553 5554 collapseElement.addEventListener("shown.bs.collapse", function () { 5555 toggleLink.textContent = "Ver menos quartos"; 5556 }); 5557 5558 collapseElement.addEventListener("hidden.bs.collapse", function () { 5559 toggleLink.textContent = "Ver mais quartos"; 5560 }); 5561 kombi.lifecycle.markInitialized(toggleLink, "room-toggle"); 5562 }); 5563}; 5564 5565(function () { 5566 const PACKAGE_POST_RESULTS_SKELETON_MS = 2000; 5567 5568 kombi.packageSearchPollingLoader = function () { 5569 const polls = [...document.querySelectorAll('#search-poll-div[data-package-flight-poll-loader="1"]')]; 5570 if (polls.length === 0) { 5571 return; 5572 }
5573 polls.forEach(function (pollDiv) { 5574 if (kombi.flight && kombi.flight.initLoaderTextRotation) { 5575 kombi.flight.initLoaderTextRotation(pollDiv); 5576 } 5577 }); 5578 }; 5579 5580 function revealPackageSearchPostResultsSkeleton(root) { 5581 if (!root || root.getAttribute("data-package-post-results-skeleton") !== "1") { 5582 return; 5583 } 5584 window.setTimeout(function () { 5585 const live = document.getElementById("search-poll-div"); 5586 if (!live || live.getAttribute("data-package-post-results-skeleton") !== "1") { 5587 return; 5588 } 5589 const skeleton = live.querySelector(".package-search-post-results-skeleton"); 5590 const panel = live.querySelector(".hotel-search-results-panel"); 5591 if (!skeleton || !panel) { 5592 return; 5593 } 5594 skeleton.classList.add("d-none"); 5595 panel.classList.remove("d-none"); 5596 live.removeAttribute("data-package-post-results-skeleton"); 5597 }
5597, PACKAGE_POST_RESULTS_SKELETON_MS); 5598 } 5599 5600 // outerHTML swap: event.detail.target is the replaced (detached) node. 5601 // Always resolve #search-poll-div from the live DOM for skeleton reveal. 5602 kombi.lifecycle.onBody( 5603 "htmx:afterSwap", 5604 function () { 5605 kombi.packageSearchPollingLoader(); 5606 const root = document.getElementById("search-poll-div"); 5607 if (root) { 5608 revealPackageSearchPostResultsSkeleton(root); 5609 } 5610 }, 5611 { targetId: "search-poll-div", onceKey: "hotel-package-poll" } 5612 ); 5613})(); 5614; 5615kombi.selectRooms = function(button) { 5616 if (!button) { 5617 return; 5618 } 5619 5620 const accordion = document.getElementById('accordionResulOccupation'); 5621 if (accordion) { 5622 accordion.scrollIntoView({ behavior: 'smooth', block: 'start' }); 5623 const firstItem = accordion.querySelector('.accordion-collapse'); 5624 if (firstItem && !firstItem.classList.contains('show')) { 5625 new bootstrap.Collapse(firstItem, { toggle: true }); 5626 } 5627 } 5628 5629 const switchButtons = button.closest('#switch-buttons'); 5630 const cartButton = switchButtons.querySelector('#cart-button'); 5631 if(cartButton) { 5632 button.classList.add('d-none') 5633 cartButton.classList.remove('d-none'); 5634 } 5635} 5636 5637kombi.highlightElement = function() { 5638 const el = document.querySelector(".d-sm-block .nav-search"); 5639 el.classList.add("highlight"); 5640 5641 setTimeout(() => { 5642 el.classList.remove("highlight"); 5643 }, 2000); 5644} 5645 5646kombi.clearSearchField = function(field) { 5647 if (field == "guests") { 5648 const clearGuestsButtons = document.querySelectorAll(".clear-guests .closeIcon") 5649 clearGuestsButtons.forEach(button => { 5650 button.click() 5651 }) 5652 } else { 5653 const clearCalendarButtons = document.querySelectorAll(".clear-calendar") 5654 clearCalendarButtons.forEach(button => { 5655 button.click() 5656 }) 5657 } 5658} 5659kombi.hideCartToast = function() { 5660 const cartToast = document.getElementById("cart-toast"); 5661 cartToast?.classList.add("d-none"); 5662} 5663 5664kombi.suppressCartToast = function() { 5665 const cartToast = document.getElementById("cart-toast"); 5666 if (!cartToast) return; 5667 5668 cartToast.dataset.displayToast = "false"; 5669 kombi.hideCartToast(); 5670} 5671 5672kombi.changeSearch = function(field, options) { 5673 if (field && typeof field === "object") { 5674 options = field; 5675 field = undefined; 5676 } 5677 options = options || {}; 5678 5679 if (options.suppressCartToast) { 5680 kombi.suppressCartToast(); 5681 } 5682 5683 kombi.clearSearchField(field) 5684 5685 const offcanvasEl = document.querySelector("#malaOffcanvas") 5686 const offcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl); 5687 5688 if (offcanvasEl.classList.contains("show")) { 5689 offcanvas.hide() 5690 } 5691 5692 kombi.closeModal('malaModal'); 5693 5694 window.scrollTo({ 5695 top: 0, 5696 }); 5697 5698 if (window.matchMedia("(min-width: 992px)").matches) { 5699 kombi.highlightElement(); 5700 if (field != "guests") { 5701 const checkIfAtTop = () => { 5702 if (window.scrollY <= 5) { 5703 kombi.openDatePicker(); 5704 } else { 5705 requestAnimationFrame(checkIfAtTop); 5706 } 5707 }; 5708 requestAnimationFrame(checkIfAtTop); 5709 } 5710 } else { 5711 const modalElement = document.getElementById("SearchForm") 5712 let modalInstance = bootstrap.Modal.getInstance(modalElement) 5713 if (!modalInstance) { 5714 modalInstance = new bootstrap.Modal(modalElement) 5715 } 5716 if (field != "guests") { 5717 const showDatePickerAfterModal = () => { 5718 kombi.openDatePicker(); 5719 modalElement.removeEventListener("shown.bs.modal", showDatePickerAfterModal); 5720 }; 5721 const checkIfAtTop = () => { 5722 if (window.scrollY <= 5) { 5723 modalElement.addEventListener("shown.bs.modal", showDatePickerAfterModal); 5724 modalInstance.show(); 5725 } else { 5726 requestAnimationFrame(checkIfAtTop); 5727 } 5728 }; 5729 requestAnimationFrame(checkIfAtTop); 5730 } 5731 } 5732} 5733 5734kombi.isTravelSuitcaseOpen = function () { 5735 const malaOffcanvas = document.getElementById("malaOffcanvas"); 5736 const malaModal = document.getElementById("malaModal"); 5737 return ( 5738 !!(malaOffcanvas && malaOffcanvas.classList.contains("show")) || 5739 !!(malaModal && malaModal.classList.contains("show")) 5740 ); 5741}; 5742 5743kombi.showCartToastIfFull = function() { 5744 const cartToast = document.getElementById("cart-toast"); 5745 if (!cartToast) return; 5746 // afterOnLoad can race with cart:open-offcanvas (afterSettle) 5747 if (kombi.isTravelSuitcaseOpen()) return; 5748 5749 const isCartFull = cartToast.dataset.isCartFull === "true"; 5750 const dataDisplay = cartToast.dataset.displayToast === "true"; 5751 if (isCartFull && dataDisplay) { 5752 cartToast.classList.remove("d-none"); 5753 } 5754} 5755 5756kombi.travelSuitcaseNavLinkMobile = function() { 5757 // Delegate on body: #travel-suitcase innerHTML swap replaces #malaOffcanvas, 5758 // so listeners on the element itself would be lost after bindOnce. 5759 kombi.lifecycle.bindOnce("travel-suitcase-mobile", function () { 5760 const EXTRA_MODALS = 5761 "#PackageModal, #PackageModalMoreThan9Guests, [id^=\"FlightModal\"]"; 5762 5763 function isExtraModal(el) { 5764 return !!(el && el.matches && el.matches(EXTRA_MODALS)); 5765 } 5766 5767 function getMalaNav() { 5768 const malaLink = document.querySelector( 5769 '.nav-link[data-bs-target="#malaOffcanvas"]' 5770 ); 5771 return { 5772 malaLink: malaLink, 5773 statusColor: malaLink?.querySelector(".status-color"), 5774 }; 5775 } 5776 5777 document.body.addEventListener("shown.bs.offcanvas", function (event) { 5778 if (event.target.id !== "malaOffcanvas") return; 5779 const nav = getMalaNav(); 5780 nav.malaLink?.classList.add("active"); 5781 nav.statusColor?.classList.add("d-none"); 5782 kombi.hideCartToast(); 5783 }); 5784 5785 document.body.addEventListener("hidden.bs.offcanvas", function (event) { 5786 if (event.target.id !== "malaOffcanvas") return; 5787 const nav = getMalaNav(); 5788 nav.malaLink?.classList.remove("active"); 5789 nav.statusColor?.classList.remove("d-none"); 5790 kombi.showCartToastIfFull(); 5791 }); 5792 5793 document.body.addEventListener("shown.bs.modal", function (event) { 5794 if (!isExtraModal(event.target)) return; 5795 kombi.hideCartToast(); 5796 }); 5797 5798 document.body.addEventListener("hidden.bs.modal", function (event) { 5799 if (!isExtraModal(event.target)) return; 5800 kombi.showCartToastIfFull(); 5801 }); 5802 }); 5803} 5804 5805kombi.travelSuitcaseNavLinkDesktop = function() { 5806 // Delegate on body: #travel-suitcase swap recreates #malaModal. 5807 // Extra modals toast hide/show is already handled by travelSuitcaseNavLinkMobile. 5808 kombi.lifecycle.bindOnce("travel-suitcase-desktop", function () { 5809 function getMalaNav() { 5810 const malaItem = document.querySelector( 5811 'li[data-bs-target="#malaModal"]' 5812 ); 5813 return { 5814 malaItem: malaItem, 5815 statusColor: malaItem?.querySelector(".status-color"), 5816 }; 5817 } 5818 5819 document.body.addEventListener("shown.bs.modal", function (event) { 5820 if (event.target.id !== "malaModal") return; 5821 const nav = getMalaNav(); 5822 nav.malaItem?.classList.add("text-primary"); 5823 nav.statusColor?.classList.add("d-none"); 5824 kombi.hideCartToast(); 5825 }); 5826 5827 document.body.addEventListener("hidden.bs.modal", function (event) { 5828 if (event.target.id !== "malaModal") return; 5829 const nav = getMalaNav(); 5830 nav.malaItem?.classList.remove("text-primary"); 5831 nav.statusColor?.classList.remove("d-none"); 5832 kombi.showCartToastIfFull(); 5833 }); 5834 }); 5835}; 5836kombi.toast = function (toastId) { 5837 const toast = document.getElementById(toastId); 5838 if (!toast) return; 5839 toast.classList.remove("d-none"); 5840 setTimeout(() => { 5841 toast.classList.add("d-none"); 5842 }, 5000); 5843};; 5844kombi.closeOffcanvas = function(offcanvasId) { 5845 const offcanvasElement = document.getElementById(offcanvasId); 5846 const offcanvasInstance = offcanvasElement && bootstrap.Offcanvas.getInstance(offcanvasElement); 5847 if (offcanvasInstance) { 5848 offcanvasInstance.hide(); 5849 } 5850}; 5851; 5852kombi.closeModal = function(modalId) { 5853 const modalElement = document.getElementById(modalId); 5854 const modalInstance = modalElement && bootstrap.Modal.getInstance(modalElement); 5855 if (modalInstance) modalInstance.hide(); 5856} 5857//# sourceMappingURL=kombi-all.js.map
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.