1/** 2 * InsideMiamiBeach 2026 - Main JS v1.5 3 * Zero jQuery. 4 */ 5(function () { 6 7 'use strict'; 8 9 /* ========================================================================== 10 1. MEGA MENU â stacked rows: L2 row + L3 row 11 ========================================================================== */ 12 var mainNav = document.querySelector('.main-nav'); 13 if (mainNav) { 14 var navList = mainNav.querySelector('.main-nav__list') || mainNav.querySelector('.menu'); 15 var topParents = navList ? navList.querySelectorAll(':scope > .menu-item-has-children') : []; 16 var activeL1 = null; 17 var activeL2 = null; 18 19 function getNavCenter() { 20 var w = mainNav.querySelector('.wrapper'); 21 var r = w ? w.getBoundingClientRect() : mainNav.getBoundingClientRect(); 22 return r.left + r.width / 2; 23 } 24 25 function positionSub(li, sub) { 26 var liRect = li.getBoundingClientRect(); 27 var navCenter = getNavCenter(); 28 var subWidth = Math.min(1200, window.innerWidth); 29 sub.style.left = (navCenter - liRect.left - subWidth / 2) + 'px'; 30 sub.style.width = subWidth + 'px'; 31 sub.style.transform = 'none'; 32 } 33 34 function closeL3() { 35 if (activeL2) { 36 activeL2.classList.remove('is-open'); 37 activeL2 = null; 38 } 39 mainNav.querySelectorAll('.sub-menu > li.is-highlight').forEach(function (el) { 40 el.classList.remove('is-highlight'); 41 }); 42 } 43 44 function closeL2() { 45 closeL3(); 46 if (activeL1) { 47 activeL1.classList.remove('is-open'); 48 activeL1 = null; 49 } 50 } 51 52 topParents.forEach(function (li) { 53 var link = li.querySelector(':scope > a'); 54 var sub = li.querySelector(':scope > .sub-menu'); 55 if (!link || !sub) return; 56 57 link.addEventListener('click', function (e) { 58 var href = link.getAttribute('href'); 59 if (!href || href === '#' || href === '#0') e.preventDefault(); 60 61 if (activeL1 === li) { 62 closeL2(); 63 } else { 64 closeL2(); 65 li.classList.add('is-open'); 66 positionSub(li, sub); 67 activeL1 = li; 68 69 var subParents = sub.querySelectorAll(':scope > li.menu-item-has-children'); 70 subParents.forEach(function (subLi) { 71 var subLink = subLi.querySelector(':scope > a'); 72 var subSub = subLi.querySelector(':scope > .sub-menu'); 73 if (!subLink || !subSub) return; 74 75 var newLink = subLink.cloneNode(true); 76 subLink.parentNode.replaceChild(newLink, subLink); 77 78 newLink.addEventListener('click', function (ev) { 79 ev.preventDefault(); 80 ev.stopPropagation(); 81 82 if (activeL2 === subLi) { 83 closeL3(); 84 } else { 85 closeL3(); 86 subLi.classList.add('is-highlight'); 87 subLi.classList.add('is-open'); 88 positionSub(subLi, subSub); 89 activeL2 = subLi; 90 } 91 }); 92 }); 93 } 94 }); 95 }); 96 97 document.addEventListener('click', function (e) { 98 if (activeL1 && !mainNav.contains(e.target)) closeL2(); 99 }); 100 } 101 102 103 /* ========================================================================== 104 2. HERO SLIDER 105 ========================================================================== */ 106 var slider = document.getElementById('hero-slider'); 107 var track = document.getElementById('hero-slider-track'); 108 var prevBtn = document.getElementById('slider-prev'); 109 var nextBtn = document.getElementById('slider-next'); 110 var dotsWrap = document.getElementById('slider-dots'); 111 112 if (slider && track) { 113 var slides = track.children; 114 var total = slides.length; 115 var current = 0; 116 var autoTimer = null; 117 var isDragging = false; 118 var startX = 0; 119 var prevTranslate = 0; 120 var dragThreshold = 50; 121 var sliderWidth = 0; // cached at dragStart to avoid forced reflow in dragMove 122 123 function goTo(index, smooth) { 124 if (index < 0) index = total - 1; 125 if (index >= total) index = 0; 126 current = index; 127 var offset = -(current * (100 / total)); 128 track.style.transition = smooth !== false ? 'transform .5s ease' : 'none'; 129 track.style.transform = 'translateX(' + offset + '%)'; 130 prevTranslate = offset; 131 updateDots(); 132 } 133 function updateDots() { 134 if (!dotsWrap) return; 135 var dots = dotsWrap.children; 136 for (var i = 0; i < dots.length; i++) dots[i].classList.toggle('is-active', i === current); 137 } 138 function startAuto() { stopAuto(); autoTimer = setInterval(function () { goTo(current + 1); }, 5000); } 139 function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } } 140 141 if (prevBtn) prevBtn.addEventListener('click', function () { stopAuto(); goTo(current - 1); startAuto(); }); 142 if (nextBtn) nextBtn.addEventListener('click', function () { stopAuto(); goTo(current + 1); startAuto(); }); 143 if (dotsWrap) dotsWrap.addEventListener('click', function (e) { 144 var dot = e.target.closest('.hero-slider__dot'); 145 if (dot) { stopAuto(); goTo(parseInt(dot.dataset.index)); startAuto(); } 146 }); 147 148 function getX(e) { return e.type.indexOf('touch') >= 0 ? e.touches[0].clientX : e.clientX; } 149 function dragStart(e) { isDragging = true; startX = getX(e); sliderWidth = slider.offsetWidth; stopAuto();
149 track.style.transition = 'none'; } 150 function dragMove(e) { 151 if (!isDragging) return; 152 var pctDiff = ((getX(e) - startX) / sliderWidth) * (100 / total); 153 track.style.transform = 'translateX(' + (prevTranslate + pctDiff) + '%)'; 154 } 155 function dragEnd(e) { 156 if (!isDragging) return; 157 isDragging = false; 158 var endX = e.type.indexOf('touch') >= 0 ? e.changedTouches[0].clientX : e.clientX; 159 var diff = endX - startX; 160 if (diff < -dragThreshold) goTo(current + 1); 161 else if (diff > dragThreshold) goTo(current - 1); 162 else goTo(current); 163 startAuto(); 164 } 165 166 track.addEventListener('mousedown', dragStart); 167 track.addEventListener('mousemove', dragMove); 168 track.addEventListener('mouseup', dragEnd); 169 track.addEventListener('mouseleave', function () { if (isDragging) dragEnd({ clientX: startX, type: 'mouse' }); }); 170 track.addEventListener('touchstart', dragStart, { passive: true }); 171 track.addEventListener('touchmove', dragMove, { passive: true }); 172 track.addEventListener('touchend', dragEnd); 173 track.addEventListener('dragstart', function (e) { e.preventDefault(); }); 174
175 track.style.animation = 'none'; 176 track.style.width = (total * 100) + '%'; 177 for (var s = 0; s < total; s++) slides[s].style.width = (100 / total) + '%'; 178 goTo(0, false); 179 startAuto(); 180 slider.addEventListener('mouseenter', stopAuto); 181 slider.addEventListener('mouseleave', startAuto); 182 } 183 184 185 /* ========================================================================== 186 3. MOBILE MENU 187 ========================================================================== */ 188 var toggle = document.querySelector('.mobile-menu-toggle'); 189 var offcanvas = document.getElementById('offcanvas-menu'); 190 var overlay = document.getElementById('offcanvas-overlay'); 191 var closeBtn = offcanvas ? offcanvas.querySelector('.offcanvas__close') : null; 192 193 function openMenu() { 194 if (!offcanvas) return; 195 offcanvas.classList.add('is-open'); 196 offcanvas.setAttribute('aria-hidden', 'false'); 197 overlay.classList.add('is-open'); 198 toggle.setAttribute('aria-expanded', 'true'); 199 document.body.style.overflow = 'hidden'; 200 } 201 function closeMenu() { 202 if (!offcanvas) return; 203 offcanvas.classList.remove('is-open'); 204 offcanvas.setAttribute('aria-hidden', 'true'); 205 overlay.classList.remove('is-open'); 206 toggle.setAttribute('aria-expanded', 'false'); 207 document.body.style.overflow = ''; 208 } 209 if (toggle) toggle.addEventListener('click', function () { 210 offcanvas.classList.contains('is-open') ? closeMenu() : openMenu(); 211 }); 212 if (overlay) overlay.addEventListener('click', closeMenu); 213 if (closeBtn) closeBtn.addEventListener('click', closeMenu); 214 215 var mobileParents = offcanvas ? offcanvas.querySelectorAll('.menu-item-has-children > a') : []; 216 mobileParents.forEach(function (link) { 217 link.addEventListener('click', function (e) { 218 var sub = this.closest('li').querySelector('.sub-menu'); 219 if (sub) { 220 e.preventDefault(); 221 sub.classList.toggle('is-open'); 222 this.classList.toggle('is-open'); 223 } 224 }); 225 }); 226 227 228 /* ========================================================================== 229 3b. LANGUAGE POPUP 230 ========================================================================== */ 231 var langTrigger = document.querySelector('.lang-trigger'); 232 var langPopup = document.getElementById('lang-popup'); 233 // Move popup to <body> so position:fixed works correctly regardless of parent transforms 234 if (langPopup) document.body.appendChild(langPopup); 235 var langClose = langPopup ? langPopup.querySelector('.lang-popup__close') : null; 236 var langBackdrop = langPopup ? langPopup.querySelector('.lang-popup__backdrop') : null; 237 238 function openLang() { 239 if (!langPopup) return; 240 langPopup.classList.add('is-open'); 241 langPopup.setAttribute('aria-hidden', 'false'); 242 if (langTrigger) langTrigger.setAttribute('aria-expanded', 'true'); 243 document.body.style.overflow = 'hidden'; 244 } 245 function closeLang() { 246 if (!langPopup) return; 247 langPopup.classList.remove('is-open'); 248 langPopup.setAttribute('aria-hidden', 'true'); 249 if (langTrigger) langTrigger.setAttribute('aria-expanded', 'false'); 250 document.body.style.overflow = ''; 251 } 252 if (langTrigger) langTrigger.addEventListener('click', openLang); 253 if (langClose) langClose.addEventListener('click', closeLang); 254 if (langBackdrop) langBackdrop.addEventListener('click', closeLang); 255 256 /* ========================================================================== 257 3c. SEARCH OVERLAY TOGGLE 258 ========================================================================== */ 259 var searchToggle = document.querySelector('.search-toggle'); 260 var searchOverlay = document.getElementById('search-overlay'); 261 var searchClose = document.getElementById('search-overlay-close'); 262 if (searchToggle && searchOverlay) { 263 searchToggle.addEventListener('click', function (e) { 264 e.preventDefault(); 265 searchOverlay.classList.add('is-open'); 266 document.body.style.overflow = 'hidden'; 267 var input = searchOverlay.querySelector('input[type="search"]'); 268 if (input) setTimeout(function () { input.focus(); }, 100); 269 });
270 if (searchClose) searchClose.addEventListener('click', function () { 271 searchOverlay.classList.remove('is-open'); 272 document.body.style.overflow = ''; 273 }); 274 searchOverlay.addEventListener('click', function (e) { 275 if (e.target === searchOverlay) { 276 searchOverlay.classList.remove('is-open'); 277 document.body.style.overflow = ''; 278 } 279 }); 280 } 281 282 283 /* ========================================================================== 284 3c. ACCORDION TOGGLE 285 ========================================================================== */ 286 document.querySelectorAll('.su-spoiler-title').forEach(function (title) { 287 title.addEventListener('click', function () { 288 var spoiler = this.closest('.su-spoiler'); 289 if (!spoiler) return; 290 if (spoiler.classList.contains('su-spoiler-open')) { 291 spoiler.classList.remove('su-spoiler-open'); 292 spoiler.classList.add('su-spoiler-closed'); 293 } else { 294 spoiler.classList.remove('su-spoiler-closed'); 295 spoiler.classList.add('su-spoiler-open'); 296 } 297 }); 298 }); 299 300 301 /* ========================================================================== 302 3d. GALLERY CAROUSEL â 3-col desktop / 1-col mobile, real-time swipe 303 ========================================================================== */ 304 305 function makeCarousel(container, items, trackEl) { 306 var pos = 0; 307 var total = items.length; 308 var touching = false; 309 var touchStartX = 0; 310 var baseOffset = 0; 311 var cachedContainerWidth = 0; // cached at touchstart to avoid forced reflow in touchmove 312 313 function getVisible() { return window.innerWidth <= 768 ? 1 : 3; } 314 315 function clamp(p) { 316 return Math.max(0, Math.min(p, Math.max(0, total - getVisible()))); 317 } 318 319 function setPos(p, animate) { 320 pos = clamp(p); 321 var vis = getVisible(); 322 trackEl.style.transition = animate ? 'transform .35s cubic-bezier(.25,.46,.45,.94)' : 'none'; 323 baseOffset = -(pos * (100 / vis)); 324 trackEl.style.transform = 'translateX(' + baseOffset + '%)'; 325 } 326 327 // Nav arrows 328 var btnPrev = document.createElement('button'); 329 btnPrev.className = 'gallery-nav gallery-nav--prev'; 330 btnPrev.setAttribute('aria-label', 'Previous'); 331 btnPrev.innerHTML = '‹'; 332 var btnNext = document.createElement('button'); 333 btnNext.className = 'gallery-nav gallery-nav--next'; 334 btnNext.setAttribute('aria-label', 'Next'); 335 btnNext.innerHTML = '›'; 336 container.appendChild(btnPrev); 337 container.appendChild(btnNext); 338 btnPrev.addEventListener('click', function () { setPos(pos - 1, true); }); 339 btnNext.addEventListener('click', function () { setPos(pos + 1, true); }); 340 341 // Real-time touch drag 342 container.addEventListener('touchstart', function (e) { 343 touching = true; 344 touchStartX = e.touches[0].clientX; 345 cachedContainerWidth = container.offsetWidth; // read layout once, before any style changes 346 trackEl.style.transition = 'none'; 347 }, { passive: true }); 348 349 container.addEventListener('touchmove', function (e) { 350 if (!touching) return; 351 var diff = e.touches[0].clientX - touchStartX; 352 var vis = getVisible(); 353 var dragPct = (diff / cachedContainerWidth) * 100; 354 trackEl.style.transform = 'translateX(' + (baseOffset + dragPct) + '%)'; 355 }, { passive: true }); 356 357 container.addEventListener('touchend', function (e) { 358 if (!touching) return; 359 touching = false; 360 var diff = e.changedTouches[0].clientX - touchStartX; 361 if (diff < -50) setPos(pos + 1, true); 362 else if (diff > 50) setPos(pos - 1, true); 363 else setPos(pos, true); 364 }); 365 366 setPos(0, false); 367 window.addEventListener('resize', function () { setPos(pos, false); }); 368 } 369 370 // su-image-carousel (Ultimate Shortcodes) 371 document.querySelectorAll('.su-image-carousel').forEach(function (carousel) { 372 if (carousel.flickity) { try { carousel.flickity('destroy'); } catch (e) {} } 373 374 var flickViewport = carousel.querySelector('.flickity-viewport'); 375 if (flickViewport) { 376 var flickSlider = flickViewport.querySelector('.flickity-slider'); 377 if (flickSlider) { 378 while (flickSlider.firstChild) carousel.insertBefore(flickSlider.firstChild, flickViewport); 379 } 380 flickViewport.remove(); 381 carousel.querySelectorAll('.flickity-page-dots, .flickity-prev-next-button').forEach(function (el) { el.remove(); }); 382 } 383
384 var items = Array.from(carousel.querySelectorAll('.su-image-carousel-item')); 385 if (!items.length) return; 386 387 var trackEl = document.createElement('div'); 388 trackEl.className = 'su-image-carousel-inner'; 389 items.forEach(function (item) { trackEl.appendChild(item); }); 390 carousel.insertBefore(trackEl, carousel.firstChild); 391 carousel.classList.add('imb-carousel-ready'); 392 393 makeCarousel(carousel, items, trackEl); 394 }); 395 396 // imb-gallery (our own shortcode) 397 document.querySelectorAll('.imb-gallery').forEach(function (gallery) { 398 var items = Array.from(gallery.querySelectorAll('.imb-gallery__item')); 399 if (!items.length) return; 400 401 var trackEl = document.createElement('div'); 402 trackEl.className = 'imb-gallery__track'; 403 items.forEach(function (item) { trackEl.appendChild(item); }); 404 gallery.insertBefore(trackEl, gallery.firstChild); 405 406 makeCarousel(gallery, items, trackEl); 407 }); 408 409 410 /* ========================================================================== 411 3e. BOOKING BOX TOGGLE (called from meta box inline onclick) 412 ========================================================================== */ 413 // Booking form â intercept submit, build Stay22 URL explicitly 414 var bkForm = document.getElementById('frm_7738'); 415 if (bkForm) { 416 bkForm.addEventListener('submit', function (e) { 417 e.preventDefault(); 418 var checkin = document.getElementById('checkin_date') ? document.getElementById('checkin_date').value : ''; 419 var checkout = document.getElementById('checkout_date') ? document.getElementById('checkout_date').value : ''; 420 if (!checkin || !checkout) { 421 alert('Please select check-in and check-out dates.'); 422 return; 423 } 424 if (checkin >= checkout) { 425 alert('Check-out date must be after check-in date.'); 426 return; 427 } 428 var get = function (name) { 429 var el = bkForm.querySelector('input[name="' + name + '"]'); 430 return el ? el.value : ''; 431 }; 432 var lat = get('lat'); 433 var lng = get('lng'); 434 var url = 'https://www.stay22.com/allez/booking' 435 + '?aid=' + encodeURIComponent(get('aid')) 436 + '&hotelname=' + encodeURIComponent(get('hotelname')) 437 + '&address=' + encodeURIComponent(get('address')); 438 if (lat && lng) { 439 url += '&lat=' + encodeURIComponent(lat) + '&lng=' + encodeURIComponent(lng); 440 } 441 url += '&checkin=' + encodeURIComponent(checkin) 442 + '&checkout=' + encodeURIComponent(checkout); 443 window.open(url, '_blank'); 444 }); 445 } 446 447 window.toggleBookingSection = function () { 448 if (window.innerWidth > 768) return; // desktop: always visible via CSS 449 var section = document.getElementById('booking-section'); 450 var btn = document.querySelector('.book-now-button'); 451 if (!section) return; 452 var hidden = section.style.display === 'none' || section.style.display === ''; 453 section.style.display = hidden ? 'block' : 'none'; 454 if (btn) btn.style.display = hidden ? 'none' : 'block'; 455 }; 456 457 // Init booking box: mobile only needs explicit state 458 (function () { 459 var section = document.getElementById('booking-section'); 460 var btn = document.querySelector('.book-now-button'); 461 if (!section) return; 462 if (window.innerWidth <= 768) { 463 section.style.display = 'none'; 464 if (btn) btn.style.display = 'block'; 465 } 466 })(); 467 468 469 /* ========================================================================== 470 3f. IMB DATEPICKER â custom two-month calendar for booking bar 471 ========================================================================== */ 472 (function () { 473 var checkinDate = null; 474 var checkoutDate = null; 475 var activeField = null; 476 var viewYear = 0; 477 var viewMonth = 0; 478 var dp = null; 479 var slideDir = null; // 'left' or 'right' for swipe animation 480 481 var MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; 482 var DAYS = ['M','T','W','T','F','S','S']; 483 484 function pad(n) { return n < 10 ? '0' + n : '' + n; } 485 function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); } 486 function fmtDisp(d) { 487 var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; 488 var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; 489 return days[d.getDay()] + ', ' + months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); 490 } 491 function sameDay(a, b) { 492 return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); 493 } 494 495 function buildMonthCol(year, month, showPrev, showNext) { 496 var col = document.createElement('div'); 497 col.className = 'imb-dp-col'; 498 499 var nav = document.createElement('div'); 500 nav.className = 'imb-dp-nav'; 501 502 var prevBtn = document.createElement('button'); 503 prevBtn.type = 'button'; 504 prevBtn.className = 'imb-dp-nav-btn'; 505 if (showPrev) { 506 prevBtn.innerHTML = '‹'; 507 prevBtn.addEventListener('click', function (e) { 508 e.stopPropagation(); 509 slideDir = 'right'; 510 viewMonth--; 511 if (viewMonth < 0) { viewMonth = 11; viewYear--; }
512 buildCalendar(); 513 }); 514 } else { prevBtn.style.visibility = 'hidden'; } 515 516 var title = document.createElement('div'); 517 title.className = 'imb-dp-month-name'; 518 title.textContent = MONTHS[month] + ' ' + year; 519 520 var nextBtn = document.createElement('button'); 521 nextBtn.type = 'button'; 522 nextBtn.className = 'imb-dp-nav-btn'; 523 if (showNext) { 524 nextBtn.innerHTML = '›'; 525 nextBtn.addEventListener('click', function (e) { 526 e.stopPropagation(); 527 slideDir = 'left'; 528 viewMonth++; 529 if (viewMonth > 11) { viewMonth = 0; viewYear++; } 530 buildCalendar(); 531 }); 532 } else { nextBtn.style.visibility = 'hidden'; } 533 534 nav.appendChild(prevBtn); 535 nav.appendChild(title); 536 nav.appendChild(nextBtn); 537 col.appendChild(nav); 538 539 var grid = document.createElement('div'); 540 grid.className = 'imb-dp-grid'; 541 DAYS.forEach(function (d) { 542 var dh = document.createElement('div'); 543 dh.className = 'imb-dp-dh'; 544 dh.textContent = d; 545 grid.appendChild(dh); 546 }); 547 548 var today = new Date(); 549 today.setHours(0, 0, 0, 0); 550 var startDow = (new Date(year, month, 1).getDay() + 6) % 7; // Mon=0 551 for (var i = 0; i < startDow; i++) { 552 var empty = document.createElement('div'); 553 empty.className = 'imb-dp-cell'; 554 grid.appendChild(empty); 555 } 556 557 var daysInMonth = new Date(year, month + 1, 0).getDate(); 558 for (var day = 1; day <= daysInMonth; day++) { 559 var cellDate = new Date(year, month, day); 560 var iso = toISO(cellDate); 561 var cell = document.createElement('div'); 562 cell.className = 'imb-dp-cell imb-dp-day'; 563 cell.textContent = day; 564 cell.dataset.date = iso; 565 566 if (cellDate < today) { 567 cell.classList.add('is-past'); 568 } else { 569 if (sameDay(cellDate, today)) cell.classList.add('is-today'); 570 if (sameDay(cellDate, checkinDate)) cell.classList.add('is-selected', 'is-checkin', 'is-range-start'); 571 if (sameDay(cellDate, checkoutDate)) cell.classList.add('is-selected', 'is-checkout', 'is-range-end'); 572 if (checkinDate && checkoutDate && cellDate > checkinDate && cellDate < checkoutDate) cell.classList.add('is-range'); 573 cell.addEventListener('click', handleDayClick); 574 cell.addEventListener('mouseenter', handleDayHover); 575 cell.addEventListener('mouseleave', clearHover); 576 } 577 grid.appendChild(cell); 578 } 579 580 col.appendChild(grid); 581 return col; 582 } 583 584 function buildInner() { 585 var isMobile = window.innerWidth <= 768; 586 var monthCount = isMobile ? 1 : 2; 587 var inner = document.createElement('div'); 588 inner.className = 'imb-dp-inner' + (slideDir ? ' imb-dp-slide-' + slideDir : ''); 589 slideDir = null; 590 for (var m = 0; m < monthCount; m++) { 591 var abs = viewMonth + m; 592 var cy = viewYear + Math.floor(abs / 12); 593 var cm = ((abs % 12) + 12) % 12; 594 inner.appendChild(buildMonthCol(cy, cm, m === 0, m === monthCount - 1)); 595 } 596 return inner; 597 } 598 599 function buildCalendar() { 600 var isMobile = window.innerWidth <= 768; 601 602 // If dp already exists, just swap the inner â no flash 603 if (dp) { 604 var oldInner = dp.querySelector('.imb-dp-inner'); 605 if (oldInner) oldInner.remove(); 606 // Update header label if on mobile 607 var lbl = dp.querySelector('.imb-dp-header-label'); 608 if (lbl) lbl.textContent = activeField === 'checkout' ? 'Select check-out date' : 'Select check-in date'; 609 dp.appendChild(buildInner()); 610 return; 611 } 612 613 // First open â create everything 614 dp = document.createElement('div'); 615 dp.className = 'imb-datepicker'; 616 617 if (isMobile) { 618 var header = document.createElement('div'); 619 header.className = 'imb-dp-header'; 620 var label = document.createElement('span'); 621 label.className = 'imb-dp-header-label'; 622 label.textContent = activeField === 'checkout' ? 'Select check-out date' : 'Select check-in date'; 623 var closeBtn = document.createElement('button'); 624 closeBtn.type = 'button'; 625 closeBtn.className = 'imb-dp-close-btn'; 626 closeBtn.innerHTML = '×'; 627 closeBtn.addEventListener('click', function(e) { e.stopPropagation(); closePicker(); }); 628 header.appendChild(label); 629 header.appendChild(closeBtn); 630 dp.appendChild(header); 631 632 dp.style.top = '0'; 633 dp.style.bottom = '0'; 634 dp.style.maxHeight = 'none'; 635 dp.style.overflowY = 'auto'; 636 637 // Swipe left = next month, swipe right = prev month 638 var swipeStartX = 0; 639 var swipeStartY = 0; 640 dp.addEventListener('touchstart', function(e) { 641 swipeStartX = e.touches[0].clientX; 642 swipeStartY = e.touches[0].clientY; 643 }, { passive: true }); 644 dp.addEventListener('touchend', function(e) { 645 var dx = e.changedTouches[0].clientX - swipeStartX; 646 var dy = e.changedTouches[0].clientY - swipeStartY; 647 if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy)) return; 648 if (dx < 0) { 649 slideDir = 'left'; 650 viewMonth++; if (viewMonth > 11) { viewMonth = 0; viewYear++; } 651 } else { 652 slideDir = 'right'; 653 viewMonth--; if (viewMonth < 0) { viewMonth = 11; viewYear--; } 654 }
655 buildCalendar(); 656 }, { passive: true }); 657 658 var backdrop = document.getElementById('imb-dp-backdrop'); 659 if (!backdrop) { 660 backdrop = document.createElement('div'); 661 backdrop.className = 'imb-dp-backdrop'; 662 backdrop.id = 'imb-dp-backdrop'; 663 backdrop.addEventListener('click', closePicker); 664 document.body.appendChild(backdrop); 665 } 666 setTimeout(function () { backdrop.classList.add('is-open'); }, 10); 667 } 668 669 dp.appendChild(buildInner()); 670 document.body.appendChild(dp); 671 setTimeout(function () { if (dp) dp.classList.add('is-open'); }, 10); 672 } 673 674 function clearHover() { 675 if (!dp) return; 676 dp.querySelectorAll('.is-hover-range,.is-hover-end').forEach(function (c) { 677 c.classList.remove('is-hover-range', 'is-hover-end'); 678 }); 679 } 680 681 function handleDayHover() { 682 if (activeField !== 'checkout' || !checkinDate || !dp) return; 683 var p = this.dataset.date.split('-'); 684 var hov = new Date(+p[0], +p[1] - 1, +p[2]); 685 clearHover(); 686 if (hov <= checkinDate) return; 687 dp.querySelectorAll('.imb-dp-day:not(.is-past)').forEach(function (c) { 688 var cp = c.dataset.date.split('-'); 689 var cd = new Date(+cp[0], +cp[1] - 1, +cp[2]); 690 if (cd > checkinDate && cd < hov) c.classList.add('is-hover-range'); 691 if (cd.getTime() === hov.getTime()) c.classList.add('is-hover-end'); 692 }); 693 } 694 695 function handleDayClick(e) { 696 e.stopPropagation(); 697 var iso = this.dataset.date; 698 var p = iso.split('-'); 699 var sel = new Date(+p[0], +p[1] - 1, +p[2]); 700 var ciEl = document.getElementById('checkin_display'); 701 var coEl = document.getElementById('checkout_display'); 702 var ciIn = document.getElementById('checkin_date'); 703 var coIn = document.getElementById('checkout_date'); 704 705 if (activeField === 'checkin') { 706 checkinDate = sel; 707 if (checkoutDate && checkoutDate <= checkinDate) { 708 checkoutDate = null; 709 if (coEl) { coEl.textContent = 'Check-out date'; coEl.classList.add('is-placeholder'); } 710 if (coIn) coIn.value = ''; 711 } 712 if (ciEl) { ciEl.textContent = fmtDisp(sel); ciEl.classList.remove('is-placeholder', 'is-active'); } 713 if (ciIn) ciIn.value = iso; 714 // Auto-switch to checkout 715 activeField = 'checkout'; 716 if (coEl) coEl.classList.add('is-active'); 717 buildCalendar(); 718 719 } else { 720 // If user clicks same or earlier date than checkin, restart from that date as new checkin 721 if (checkinDate && sel <= checkinDate) { 722 checkinDate = sel; 723 checkoutDate = null; 724 if (ciEl) { ciEl.textContent = fmtDisp(sel); ciEl.classList.remove('is-placeholder'); } 725 if (ciIn) ciIn.value = iso; 726 if (coEl) { coEl.textContent = 'Check-out date'; coEl.classList.add('is-placeholder'); } 727 if (coIn) coIn.value = ''; 728 activeField = 'checkout'; 729 buildCalendar(); 730 return; 731 } 732 checkoutDate = sel; 733 if (coEl) { coEl.textContent = fmtDisp(sel); coEl.classList.remove('is-placeholder', 'is-active'); } 734 if (coIn) coIn.value = iso; 735 // Show full range highlighted for a moment before closing 736 activeField = null; 737 buildCalendar(); 738 setTimeout(closePicker, 600); 739 } 740 } 741 742 function closePicker() { 743 if (dp) { dp.remove(); dp = null; } 744 var backdrop = document.getElementById('imb-dp-backdrop'); 745 if (backdrop) backdrop.remove(); 746 var ciEl = document.getElementById('checkin_display'); 747 var coEl = document.getElementById('checkout_display'); 748 if (ciEl) ciEl.classList.remove('is-active'); 749 if (coEl) coEl.classList.remove('is-active'); 750 activeField = null; 751 } 752 753 function openPicker(field) { 754 var now = new Date(); 755 activeField = field; 756 // When opening checkout, start view at checkin month so user sees context 757 var ref = (field === 'checkout' && checkinDate) ? checkinDate 758 : (checkinDate ? checkinDate : now); 759 viewYear = ref.getFullYear(); 760 viewMonth = ref.getMonth(); 761 var ciEl = document.getElementById('checkin_display'); 762 var coEl = document.getElementById('checkout_display'); 763 if (ciEl) ciEl.classList.toggle('is-active', field === 'checkin'); 764 if (coEl) coEl.classList.toggle('is-active', field === 'checkout');
765 buildCalendar(); 766 } 767 768 function initPicker() { 769 var ciEl = document.getElementById('checkin_display'); 770 var coEl = document.getElementById('checkout_display'); 771 if (!ciEl || !coEl) return; 772 773 ciEl.addEventListener('click', function (e) { 774 e.stopPropagation(); 775 if (dp && activeField === 'checkin') { closePicker(); return; } 776 openPicker('checkin'); 777 }); 778 coEl.addEventListener('click', function (e) { 779 e.stopPropagation(); 780 if (dp && activeField === 'checkout') { closePicker(); return; } 781 openPicker('checkout'); 782 }); 783 [ciEl, coEl].forEach(function (el) { 784 el.addEventListener('keydown', function (e) { 785 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } 786 }); 787 }); 788 } 789 790 // Close on outside click 791 document.addEventListener('click', function (e) { 792 if (!dp) return; 793 var ciEl = document.getElementById('checkin_display'); 794 var coEl = document.getElementById('checkout_display'); 795 if (dp.contains(e.target)) return; 796 if (ciEl && ciEl.contains(e.target)) return; 797 if (coEl && coEl.contains(e.target)) return; 798 closePicker(); 799 }); 800 801 // Close on Escape 802 document.addEventListener('keydown', function (e) { 803 if (e.key === 'Escape' && dp) closePicker(); 804 }); 805 806 initPicker(); 807 })(); 808 809 810 /* ========================================================================== 811 4. BACK TO TOP / SLIDE DOCK / BOOKING BAR 812 ========================================================================== */ 813 var backTop = document.querySelector('.back-to-top'); 814 if (backTop) backTop.addEventListener('click', function (e) { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); }); 815 816 var dock = document.getElementById('slide-dock'); 817 var dockClose = document.getElementById('slide-dock-close'); 818 var dockShown = false; 819 if (dock) { 820 // Cache scrollHeight once (reading it inside scroll handler causes forced reflow) 821 var dockScrollThreshold = Math.max(600, Math.round((document.body.scrollHeight - window.innerHeight) * 0.35)); 822 window.addEventListener('scroll', function () { 823 if (dockShown) return; 824 if (window.scrollY > dockScrollThreshold) { dock.classList.add('is-visible'); dockShown = true; } 825 }, { passive: true }); 826 if (dockClose) dockClose.addEventListener('click', function () { dock.classList.remove('is-visible'); }); 827 } 828 829 var bookingBar = document.getElementById('booking-bar'); 830 if (bookingBar) { 831 var barShown = false; 832 window.addEventListener('scroll', function () { 833 if (barShown) return; 834 if (window.scrollY > 400) { bookingBar.classList.add('is-visible'); barShown = true; } 835 }, { passive: true }); 836 } 837 838 document.addEventListener('keydown', function (e) { 839 if (e.key === 'Escape') { 840 if (offcanvas && offcanvas.classList.contains('is-open')) closeMenu(); 841 if (mainNav && activeL1) closeL2(); 842 if (searchOverlay && searchOverlay.classList.contains('is-open')) { searchOverlay.classList.remove('is-open'); document.body.style.overflow = ''; } 843 if (langPopup && langPopup.classList.contains('is-open')) closeLang(); 844 closeLightbox(); 845 } 846 if (lightboxEl && lightboxEl.classList.contains('is-open')) { 847 if (e.key === 'ArrowLeft') showImage(lbIndex - 1); 848 if (e.key === 'ArrowRight') showImage(lbIndex + 1); 849 } 850 }); 851 852 853 /* ========================================================================== 854 5. LIGHTBOX â single clean implementation 855 ========================================================================== */ 856 var lightboxEl = null; 857 var lbImages = []; 858 var lbIndex = 0; 859 860 function createLightbox() { 861 if (lightboxEl) return; 862 lightboxEl = document.createElement('div'); 863 lightboxEl.className = 'imb-lightbox'; 864 lightboxEl.innerHTML = 865 '<button class="imb-lightbox__close" aria-label="Close">×</button>' 866 + '<button class="imb-lightbox__prev" aria-label="Previous">‹</button>' 867 + '<img class="imb-lightbox__img" src="" alt="" draggable="false">' 868 + '<button class="imb-lightbox__next" aria-label="Next">›</button>' 869 + '<div class="imb-lightbox__counter"></div>'; 870 document.body.appendChild(lightboxEl); 871 872 lightboxEl.querySelector('.imb-lightbox__close').addEventListener('click', closeLightbox); 873 lightboxEl.querySelector('.imb-lightbox__prev').addEventListener('click', function (e) { e.stopPropagation(); showImage(lbIndex - 1); }); 874 lightboxEl.querySelector('.imb-lightbox__next').addEventListener('click', function (e) { e.stopPropagation(); showImage(lbIndex + 1); }); 875 lightboxEl.addEventListener('click', function (e) { if (e.target === lightboxEl) closeLightbox(); }); 876 877 // Swipe on the whole overlay (not just the image) 878 var lbTouchStart = 0; 879 lightboxEl.addEventListener('touchstart', function (e) { 880 lbTouchStart = e.touches[0].clientX; 881 }, { passive: true }); 882 lightboxEl.addEventListener('touchend', function (e) { 883 var diff = e.changedTouches[0].clientX - lbTouchStart; 884 if (diff > 50) showImage(lbIndex - 1); 885 else if (diff < -50) showImage(lbIndex + 1); 886 }); 887 } 888 889 function showImage(idx) { 890 if (!lightboxEl || !lbImages.length) return; 891 if (idx < 0) idx = lbImages.length - 1; 892 if (idx >= lbImages.length) idx = 0; 893 lbIndex = idx; 894 var img = lightboxEl.querySelector('.imb-lightbox__img'); 895 // Fade out â swap src â fade in 896 img.style.opacity = '0'; 897 var newSrc = lbImages[idx]; 898 setTimeout(function () { 899 img.src = newSrc; 900 img.onload = function () { img.style.opacity = '1'; }; 901 // Fallback: if cached, onload may not fire 902 setTimeout(function () { img.style.opacity = '1'; }, 60); 903 }, 100); 904 lightboxEl.querySelector('.imb-lightbox__counter').textContent = (idx + 1) + ' / ' + lbImages.length; 905 } 906 907 function openLightbox(images, startIdx) { 908 createLightbox(); 909 lbImages = images; 910 // Show without fade on first open 911 lbIndex = startIdx < 0 ? 0 : startIdx >= images.length ? images.length - 1 : startIdx; 912 var img = lightboxEl.querySelector('.imb-lightbox__img'); 913 img.style.opacity = '1'; 914 img.src = lbImages[lbIndex]; 915 lightboxEl.querySelector('.imb-lightbox__counter').textContent = (lbIndex + 1) + ' / ' + lbImages.length; 916 lightboxEl.classList.add('is-open'); 917 document.body.style.overflow = 'hidden'; 918 } 919 920 function closeLightbox() { 921 if (lightboxEl && lightboxEl.classList.contains('is-open')) { 922 lightboxEl.classList.remove('is-open'); 923 document.body.style.overflow = ''; 924 } 925 } 926 927 /* ========================================================================== 928 MOBILE SECTION NAV â sticky bottom bar with anchors to h2.title sections 929 Only on screens <= 768px. Built from h2.title elements on the page. 930 ========================================================================== */ 931 (function() { 932 if (window.innerWidth > 768) return; 933 934 var headings = document.querySelectorAll('h2.title, .sub-title h2'); 935 if (headings.length < 2) return; 936 937 // Inject anchor IDs onto the parent section of each heading 938 var anchors = [];
939 headings.forEach(function(h2) { 940 var text = h2.textContent.trim(); 941 var id = 'section-' + text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); 942 var section = h2.closest('section') || h2.parentElement; 943 section.id = id; 944 anchors.push({ id: id, label: text }); 945 }); 946 947 // Build the sticky bar 948 var bar = document.createElement('nav'); 949 bar.className = 'mobile-section-nav'; 950 bar.setAttribute('aria-label', 'Jump to section'); 951 952 var inner = document.createElement('div'); 953 inner.className = 'mobile-section-nav__inner'; 954 955 anchors.forEach(function(a) { 956 var link = document.createElement('a'); 957 link.href = '#' + a.id; 958 link.className = 'mobile-section-nav__link'; 959 link.textContent = a.label; 960 link.addEventListener('click', function(e) { 961 e.preventDefault(); 962 var target = document.getElementById(a.id); 963 if (!target) return; 964 var offset = target.getBoundingClientRect().top + window.pageYOffset - 60; 965 window.scrollTo({ top: offset, behavior: 'smooth' }); 966 }); 967 inner.appendChild(link); 968 }); 969 970 bar.appendChild(inner); 971 document.body.appendChild(bar); 972 973 // Highlight active section on scroll + hide near footer 974 var footer = document.querySelector('footer') || document.querySelector('.site-footer'); 975 function onScroll() { 976 // Hide near footer 977 if (footer) { 978 var fTop = footer.getBoundingClientRect().top; 979 bar.style.opacity = fTop < 120 ? '0' : '1'; 980 bar.style.pointerEvents = fTop < 120 ? 'none' : ''; 981 } 982 // Active link 983 var scrollY = window.pageYOffset + window.innerHeight / 2; 984 var active = null; 985 anchors.forEach(function(a) { 986 var el = document.getElementById(a.id); 987 if (el && el.offsetTop <= scrollY) active = a.id; 988 }); 989 bar.querySelectorAll('.mobile-section-nav__link').forEach(function(l) { 990 l.classList.toggle('is-active', l.getAttribute('href') === '#' + active); 991 }); 992 } 993 window.addEventListener('scroll', onScroll, { passive: true }); 994 onScroll(); 995 })(); 996 997 /* ========================================================================== 998 LOOK CARD â thumbnail click swaps main image in place 999 ========================================================================== */ 1000 document.addEventListener( 'click', function( e ) { 1001 var btn = e.target.closest( '.look-card__thumb' ); 1002 if ( ! btn ) return; 1003 e.stopPropagation(); 1004 var card = btn.closest( '.look-card' ); 1005 if ( ! card ) return; 1006 var mainImg = card.querySelector( '.look-card__main-img' ); 1007 if ( ! mainImg ) return; 1008 1009 mainImg.src = btn.getAttribute( 'data-src' ); 1010 1011 card.querySelectorAll( '.look-card__thumb' ).forEach( function( b ) { 1012 b.classList.remove( 'is-active' ); 1013 } ); 1014 btn.classList.add( 'is-active' ); 1015 } ); 1016 1017 // Bind lightbox to gallery links â run after carousels are set up 1018 function initGalleryLightbox() { 1019 1020 // Look card thumbnails â group per card so navigation stays within the card 1021 document.querySelectorAll('.look-card').forEach(function (card) { 1022 var thumbs = Array.from(card.querySelectorAll('.look-card__thumb[href]')); 1023 if (!thumbs.length) return; 1024 var srcs = thumbs.map(function (el) { return el.getAttribute('href'); }); 1025 thumbs.forEach(function (el, idx) { 1026 el.onclick = function (e) { 1027 e.preventDefault(); 1028 openLightbox(srcs, idx); 1029 }; 1030 }); 1031 }); 1032 1033 var links = Array.from(document.querySelectorAll( 1034 '.su-image-carousel-item-content a[href], .imb-gallery__item[href], .imb-gallery__item a[href]' 1035 )).filter(function (el) { 1036 var href = el.getAttribute('href'); 1037 return href && /\.(jpe?g|png|gif|webp)/i.test(href); 1038 }); 1039 1040 var srcs = links.map(function (el) { return el.getAttribute('href'); }); 1041 1042 links.forEach(function (el, idx) { 1043 el.onclick = function (e) { 1044 e.preventDefault(); 1045 e.stopImmediatePropagation(); 1046 openLightbox(srcs, idx); 1047 return false; 1048 }; 1049 }); 1050 } 1051 1052 /* Weather widget (weatherwidget.info) rewrites HTML and can add extra links later. 1053 Force nofollow on every world-weather / weatherwidget hyperlink. */ 1054 function nofollowWeatherLinks(root) { 1055 var scope = root && root.querySelectorAll ? root : document; 1056 var links = []; 1057 try { 1058 links = scope.querySelectorAll('a[href*="world-weather.info"], a[href*="weatherwidget.info"]'); 1059 } catch (e) { 1060 return; 1061 } 1062 for (var i = 0; i < links.length; i++) { 1063 links[i].setAttribute('rel', 'nofollow noopener noreferrer'); 1064 } 1065 if (root && root.tagName === 'A' && root.href && 1066 (root.href.indexOf('world-weather.info') !== -1 || root.href.indexOf('weatherwidget.info') !== -1)) { 1067 root.setAttribute('rel', 'nofollow noopener noreferrer'); 1068 } 1069 } 1070 nofollowWeatherLinks(document); 1071 if (window.MutationObserver && document.body) { 1072 new MutationObserver(function (mutations) { 1073 var need = false;
1074 for (var i = 0; i < mutations.length; i++) { 1075 if (mutations[i].addedNodes && mutations[i].addedNodes.length) { 1076 need = true; 1077 break; 1078 } 1079 } 1080 if (need) nofollowWeatherLinks(document); 1081 }).observe(document.body, { childList: true, subtree: true }); 1082 } 1083 window.addEventListener('load', function () { nofollowWeatherLinks(document); }); 1084 1085 // Small delay to let any third-party scripts finish DOM changes 1086 setTimeout(initGalleryLightbox, 300); 1087 1088 // Featherlight replacement for standalone image links 1089 document.querySelectorAll('a[data-featherlight], a[href$=".jpg"], a[href$=".jpeg"], a[href$=".png"], a[href$=".webp"]').forEach(function (link) { 1090 if (link.closest('.imb-gallery, .su-image-carousel')) return; 1091 var href = link.getAttribute('href'); 1092 if (href && /\.(jpg|jpeg|png|webp|gif)/i.test(href)) { 1093 link.addEventListener('click', function (e) { 1094 e.preventDefault(); 1095 openLightbox([href], 0); 1096 }); 1097 } 1098 }); 1099 1100})(); 1101 1102/* ========================================================================== 1103 FILTER TABS â image gallery with category filter 1104 Usage: filterImages('south-beach') / filterImages('all') 1105 ========================================================================== */ 1106function filterImages(category) { 1107 var wrappers = document.querySelectorAll('.image-wrapper'); 1108 var tabs = document.querySelectorAll('.filter-tabs .tab'); 1109 1110 wrappers.forEach(function(el) { 1111 if (category === 'all' || el.classList.contains(category)) { 1112 el.style.display = ''; 1113 } else { 1114 el.style.display = 'none'; 1115 } 1116 }); 1117
1118 tabs.forEach(function(btn) { 1119 btn.classList.toggle('active', btn.getAttribute('onclick') === "filterImages('" + category + "')"); 1120 }); 1121}
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.