window.Webflow = window.Webflow || []; window.Webflow.push(function () { const sliders = Array.from(document.querySelectorAll( ".slider-gallery-wrapper .slider-gallery" )).filter(function (slider) { return slider.querySelector(".swiper-slide.is-gallery .image-gallery"); }); // No matching gallery means no handlers, image probes or library downloads. if (!sliders.length) return; if (window.qfGalleryLightbox) { window.qfGalleryLightbox.init(); return; } const baseURL = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/"; const dimensions = new Map(); let libraryPromise; let opening = false; let activeViewer = null; function loadAsset(tag, id, url) { let element = document.getElementById(id); if (element && element.dataset.loaded === "true") { return Promise.resolve(); } return new Promise(function (resolve, reject) { const isNew = !element; element = element || document.createElement(tag); element.id = id; if (tag === "link") { element.rel = "stylesheet"; element.href = url; } else { element.src = url; element.async = true; } const timeout = setTimeout(function () { finish(new Error("Gallery asset timed out")); }, 15000); function finish(error) { clearTimeout(timeout); element.removeEventListener("load", onLoad); element.removeEventListener("error", onError); if (error) { element.remove(); reject(error); } else { element.dataset.loaded = "true"; resolve(); } } function onLoad() { finish(); } function onError() { finish(new Error("Gallery asset could not load")); } element.addEventListener("load", onLoad); element.addEventListener("error", onError); if (isNew) document.head.appendChild(element); }); } function loadLibrary() { if (!libraryPromise) { libraryPromise = Promise.all([ loadAsset("link", "qf-photoswipe-css", baseURL + "photoswipe.css"), typeof window.PhotoSwipe === "function" ? Promise.resolve() : loadAsset("script", "qf-photoswipe-js", baseURL + "umd/photoswipe.umd.min.js") ]).then(function () { if (typeof window.PhotoSwipe !== "function") { throw new Error("PhotoSwipe is unavailable"); } return window.PhotoSwipe; }).catch(function (error) { libraryPromise = null; throw error; }); } return libraryPromise; } function getSource(image) { // Webflow's src points to the original; currentSrc may be a small thumbnail. const value = image.getAttribute("data-gallery-full") || image.getAttribute("src") || image.currentSrc; if (!value) throw new Error("The photo has no source"); const url = new URL(value, document.baseURI); if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error("Unsupported photo source"); } return url.href; } function measure(src) { if (!dimensions.has(src)) { const promise = new Promise(function (resolve, reject) { const probe = new Image(); const timeout = setTimeout(function () { finish(new Error("The photo timed out")); }, 15000); function finish(error) { clearTimeout(timeout); probe.onload = null; probe.onerror = null; if (error) reject(error); else resolve({ width: probe.naturalWidth, height: probe.naturalHeight }); } probe.onload = function () { finish(probe.naturalWidth && probe.naturalHeight ? null : new Error("Invalid photo dimensions")); }; probe.onerror = function () { finish(new Error("The photo could not load")); }; probe.src = src; }).catch(function (error) { dimensions.delete(src); throw error; }); dimensions.set(src, promise); } return dimensions.get(src); } function getPhotos(slider) { return Array.from(slider.querySelectorAll(".swiper-slide.is-gallery")) .filter(function (slide) { return slide.closest(".slider-gallery") === slider; }) .map(function (slide) { return { slide: slide, image: slide.querySelector("img.image-gallery") }; }).filter(function (photo) { return photo.image; }); } async function openPhoto(slider, image, trigger) { if (opening || activeViewer) return; opening = true; trigger.setAttribute("aria-busy", "true"); const oldMessage = slider.parentElement.querySelector(".gallery-lightbox-error"); if (oldMessage) oldMessage.remove(); try { const photos = getPhotos(slider); const results = await Promise.all([ loadLibrary(), Promise.allSettled(photos.map(async function (photo) { const src = getSource(photo.image); const size = await measure(src); return { src: src, width: size.width, height: size.height, alt: photo.image.alt || "Gallery photo", image: photo.image, slide: photo.slide }; })) ]); if (!slider.isConnected || !trigger.isConnected) return; const PhotoSwipe = results[0]; const data = results[1].filter(function (result) { return result.status === "fulfilled"; }).map(function (result) { return result.value; }); const index = data.findIndex(function (photo) { return photo.image === image; }); if (index < 0) throw new Error("The selected photo could not load"); const swiper = slider.swiper; const keyboardEnabled = Boolean(swiper && swiper.keyboard && swiper.keyboard.enabled); const autoplayRunning = Boolean(swiper && swiper.autoplay && swiper.autoplay.running && !swiper.autoplay.paused); if (keyboardEnabled) swiper.keyboard.disable(); if (autoplayRunning) swiper.autoplay.pause(); const duration = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 200; trigger.focus({ preventScroll: true }); const viewer = new PhotoSwipe({ dataSource: data, index: index, mainClass: "qf-gallery-lightbox", bgOpacity: 0.94, showHideAnimationType: "fade", showAnimationDuration: duration, hideAnimationDuration: duration, zoomAnimationDuration: duration, trapFocus: true, returnFocus: true, escKey: true, arrowKeys: true, wheelToZoom: true, preload: [1, 1] }); activeViewer = viewer; viewer.on("destroy", function () { activeViewer = null; if (swiper && !swiper.destroyed) { const selected = data[viewer.currIndex]; if (selected) { const slides = Array.from(slider.querySelectorAll(".swiper-slide.is-gallery")); const slideIndex = slides.indexOf(selected.slide); if (slideIndex >= 0) swiper.slideTo(slideIndex, 0); } if (keyboardEnabled) swiper.keyboard.enable(); if (autoplayRunning) swiper.autoplay.resume(); } if (trigger.isConnected) trigger.focus({ preventScroll: true }); }); viewer.init(); } catch (error) { if (activeViewer) activeViewer.destroy(); const message = document.createElement("p"); message.className = "gallery-lightbox-error"; message.setAttribute("role", "alert"); message.textContent = "Unable to open the photo. Please try again. "; try { const link = document.createElement("a"); link.href = getSource(image); link.target = "_blank"; link.rel = "noopener"; link.textContent = "Open original image"; message.appendChild(link); } catch (_) { /* A missing source has no fallback link. */ } slider.insertAdjacentElement("afterend", message); console.warn("Gallery lightbox:", error); } finally { opening = false; trigger.removeAttribute("aria-busy"); } } function init() { document.querySelectorAll(".slider-gallery-wrapper .slider-gallery").forEach(function (slider) { getPhotos(slider).forEach(function (photo, index) { const card = photo.image.closest(".card-slide-gallery"); if (!card || card.closest(".slider-gallery") !== slider || card.querySelector(".gallery-lightbox-trigger")) return; // Swiper blocks dragging from focused native buttons. const trigger = document.createElement("div"); trigger.setAttribute("role", "button"); trigger.tabIndex = 0; trigger.className = "gallery-lightbox-trigger"; trigger.setAttribute("aria-haspopup", "dialog"); trigger.setAttribute("aria-label", photo.image.alt ? "Open photo: " + photo.image.alt : "Open gallery photo " + (index + 1)); let start = null; let dragged = false; trigger.addEventListener("pointerdown", function (event) { start = { x: event.clientX, y: event.clientY }; dragged = false; }); trigger.addEventListener("pointermove", function (event) { if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) > 8) dragged = true; }); trigger.addEventListener("pointerup", function (event) { if (start && Math.hypot(event.clientX - start.x, event.clientY - start.y) > 8) dragged = true; start = null; }); trigger.addEventListener("pointercancel", function () { dragged = true; start = null; }); trigger.addEventListener("click", function (event) { if (event.detail > 0 && (dragged || (slider.swiper && slider.swiper.allowClick === false))) return; openPhoto(slider, photo.image, trigger); }); trigger.addEventListener("keydown", function (event) { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); if (!event.repeat) openPhoto(slider, photo.image, trigger); }); card.appendChild(trigger); }); }); } window.qfGalleryLightbox = { init: init }; init(); });