gallery - ditched alpine
This commit is contained in:
parent
aa7c039163
commit
04b211d2a3
@ -229,37 +229,7 @@ if (groupingFunction) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div
|
<div class="masonry-gallery" data-total={finalImages.length}>
|
||||||
x-data={`{
|
|
||||||
open: false,
|
|
||||||
currentIndex: 0,
|
|
||||||
total: ${finalImages.length},
|
|
||||||
touchStartX: 0,
|
|
||||||
touchEndX: 0,
|
|
||||||
minSwipeDistance: 50,
|
|
||||||
isSwiping: false,
|
|
||||||
images: ${JSON.stringify(finalImages)},
|
|
||||||
openLightbox(index) {
|
|
||||||
this.currentIndex = index;
|
|
||||||
this.open = true;
|
|
||||||
},
|
|
||||||
handleSwipe() {
|
|
||||||
if (!this.isSwiping) return;
|
|
||||||
const swipeDistance = this.touchEndX - this.touchStartX;
|
|
||||||
if (Math.abs(swipeDistance) >= this.minSwipeDistance) {
|
|
||||||
if (swipeDistance > 0 && this.currentIndex > 0) {
|
|
||||||
this.currentIndex--;
|
|
||||||
} else if (swipeDistance < 0 && this.currentIndex < this.total - 1) {
|
|
||||||
this.currentIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.isSwiping = false;
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
@keydown.escape.window="open = false"
|
|
||||||
@keydown.window="if(open){ if($event.key === 'ArrowRight' && currentIndex < total - 1){ currentIndex++; } else if($event.key === 'ArrowLeft' && currentIndex > 0){ currentIndex--; } }"
|
|
||||||
class="masonry-gallery"
|
|
||||||
>
|
|
||||||
{groupingFunction ? (
|
{groupingFunction ? (
|
||||||
<!-- Masonry Grid with Group Headers -->
|
<!-- Masonry Grid with Group Headers -->
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
@ -289,14 +259,14 @@ if (groupingFunction) {
|
|||||||
const globalIndex = finalImages.findIndex(img => img.src === image.src);
|
const globalIndex = finalImages.findIndex(img => img.src === image.src);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="masonry-item cursor-pointer group"
|
class="masonry-item cursor-pointer group masonry-trigger"
|
||||||
|
data-index={globalIndex}
|
||||||
style={`max-width: ${maxWidth}; max-height: ${maxHeight};`}
|
style={`max-width: ${maxWidth}; max-height: ${maxHeight};`}
|
||||||
x-on:click={`openLightbox(${globalIndex})`}
|
|
||||||
>
|
>
|
||||||
<div class="relative overflow-hidden rounded-lg bg-gray-100">
|
<div class="relative overflow-hidden rounded-lg bg-gray-100">
|
||||||
<Img
|
<Img
|
||||||
src={image.src}
|
src={image.src}
|
||||||
alt={image.alt}
|
alt={image.alt || ""}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
format="avif"
|
format="avif"
|
||||||
placeholder="blurred"
|
placeholder="blurred"
|
||||||
@ -349,14 +319,14 @@ if (groupingFunction) {
|
|||||||
>
|
>
|
||||||
{finalImages.map((image, index) => (
|
{finalImages.map((image, index) => (
|
||||||
<div
|
<div
|
||||||
class="masonry-item cursor-pointer group"
|
class="masonry-item cursor-pointer group masonry-trigger"
|
||||||
|
data-index={index}
|
||||||
style={`max-width: ${maxWidth}; max-height: ${maxHeight};`}
|
style={`max-width: ${maxWidth}; max-height: ${maxHeight};`}
|
||||||
x-on:click={`openLightbox(${index})`}
|
|
||||||
>
|
>
|
||||||
<div class="relative overflow-hidden rounded-lg bg-gray-100">
|
<div class="relative overflow-hidden rounded-lg bg-gray-100">
|
||||||
<Img
|
<Img
|
||||||
src={image.src}
|
src={image.src}
|
||||||
alt={image.alt}
|
alt={image.alt || ""}
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
format="avif"
|
format="avif"
|
||||||
placeholder="blurred"
|
placeholder="blurred"
|
||||||
@ -395,56 +365,57 @@ if (groupingFunction) {
|
|||||||
|
|
||||||
<!-- Lightbox Modal -->
|
<!-- Lightbox Modal -->
|
||||||
<div
|
<div
|
||||||
x-show="open"
|
class="product-gallery-lightbox fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[9999] hidden opacity-0 transition-opacity duration-300"
|
||||||
x-transition
|
role="dialog"
|
||||||
class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
|
aria-modal="true"
|
||||||
style="z-index: 9999;"
|
>
|
||||||
>
|
<div
|
||||||
<div
|
class="product-gallery-lightbox-content relative w-full h-full max-w-full max-h-full transition-transform duration-100 ease-out"
|
||||||
class="relative max-w-full max-h-full"
|
|
||||||
@touchstart="touchStartX = $event.touches[0].clientX; isSwiping = true;"
|
|
||||||
@touchend="touchEndX = $event.changedTouches[0].clientX; handleSwipe();"
|
|
||||||
@touchcancel="isSwiping = false;"
|
|
||||||
>
|
>
|
||||||
{finalImages.map((image, index) => (
|
{finalImages.map((image, index) => (
|
||||||
<div x-show={`currentIndex === ${index}`} class="flex items-center justify-center">
|
<div
|
||||||
<div class="relative">
|
class={`lightbox-slide absolute inset-0 flex items-center justify-center transition-opacity duration-300 ${index === 0 ? "opacity-100 z-10" : "opacity-0 z-0 hidden"}`}
|
||||||
<Img
|
data-index={index}
|
||||||
src={image.src}
|
>
|
||||||
alt={image.alt}
|
<div class="relative max-w-full max-h-full flex items-center justify-center w-full h-full p-4">
|
||||||
placeholder="blurred"
|
{/* Wrappers to help centering and prevent overflow issues */}
|
||||||
format="avif"
|
<div class="relative w-auto h-auto max-w-full max-h-full">
|
||||||
objectFit="contain"
|
<Img
|
||||||
sizes={mergedLightboxSettings.SIZES_LARGE}
|
src={image.src}
|
||||||
attributes={{
|
alt={image.alt || ""}
|
||||||
img: {
|
placeholder="blurred"
|
||||||
class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg",
|
format="avif"
|
||||||
style: "display: block !important; width: auto; height: auto;"
|
objectFit="contain"
|
||||||
}
|
sizes={mergedLightboxSettings.SIZES_LARGE}
|
||||||
}}
|
attributes={{
|
||||||
/>
|
img: {
|
||||||
{(mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) && (
|
class: "max-w-[90vw] max-h-[90vh] object-contain rounded-lg",
|
||||||
<div class="absolute bottom-0 left-0 right-0 m-[8px] max-h-[32vh] text-white bg-black/50 rounded-lg p2">
|
style: "display: block; width: auto; height: auto;"
|
||||||
{mergedLightboxSettings.SHOW_TITLE && image.title && (
|
}
|
||||||
<h3 class="text-xl mb-2">
|
}}
|
||||||
<Translate>{image.title}</Translate>
|
/>
|
||||||
</h3>
|
{(mergedLightboxSettings.SHOW_TITLE || mergedLightboxSettings.SHOW_DESCRIPTION) && (
|
||||||
|
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 max-w-[90%] max-h-[30vh] overflow-y-auto text-white bg-black/60 backdrop-blur-sm rounded-lg p-4 text-center">
|
||||||
|
{mergedLightboxSettings.SHOW_TITLE && image.title && (
|
||||||
|
<h3 class="text-xl font-bold mb-1">
|
||||||
|
<Translate>{image.title}</Translate>
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
{mergedLightboxSettings.SHOW_DESCRIPTION && image.description && (
|
||||||
|
<p class="text-base text-gray-200">
|
||||||
|
<Translate>{image.description}</Translate>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{mergedLightboxSettings.SHOW_DESCRIPTION && image.description && (
|
</div>
|
||||||
<p>
|
|
||||||
<Translate>{image.description}</Translate>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<!-- Close Button -->
|
<!-- Close Button -->
|
||||||
<button
|
<button
|
||||||
x-on:click="open = false"
|
class="lightbox-close absolute top-0 right-0 text-white text-2xl p-4 m-2 bg-gray-800/75 rounded-lg hover:bg-gray-700/75 transition-colors z-50 cursor-pointer"
|
||||||
class="absolute top-0 right-0 text-white text-2xl p-4 m-2 bg-gray-800/75 rounded-lg hover:bg-gray-700/75 transition-colors"
|
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@ -452,17 +423,13 @@ if (groupingFunction) {
|
|||||||
|
|
||||||
<!-- Navigation Buttons -->
|
<!-- Navigation Buttons -->
|
||||||
<button
|
<button
|
||||||
x-show="currentIndex > 0"
|
class="lightbox-prev absolute left-0 top-1/2 transform -translate-y-1/2 p-4 m-2 text-white text-3xl bg-gray-800/75 rounded-lg hover:bg-gray-700/75 transition-colors z-50 cursor-pointer"
|
||||||
x-on:click="currentIndex--;"
|
|
||||||
class="absolute left-0 top-1/2 transform -translate-y-1/2 p-4 m-2 text-white text-3xl bg-gray-800/75 rounded-lg hover:bg-gray-700/75 transition-colors"
|
|
||||||
aria-label="Previous"
|
aria-label="Previous"
|
||||||
>
|
>
|
||||||
❮
|
❮
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
x-show="currentIndex < total - 1"
|
class="lightbox-next absolute right-0 top-1/2 transform -translate-y-1/2 p-4 m-2 text-white text-3xl bg-gray-800/75 rounded-lg hover:bg-gray-700/75 transition-colors z-50 cursor-pointer"
|
||||||
x-on:click="currentIndex++;"
|
|
||||||
class="absolute right-0 top-1/2 transform -translate-y-1/2 p-4 m-2 text-white text-3xl bg-gray-800/75 rounded-lg hover:bg-gray-700/75 transition-colors"
|
|
||||||
aria-label="Next"
|
aria-label="Next"
|
||||||
>
|
>
|
||||||
❯
|
❯
|
||||||
@ -476,15 +443,14 @@ if (groupingFunction) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ... grid styles remain same ... */
|
||||||
.masonry-container {
|
.masonry-container {
|
||||||
/* CSS Grid Masonry fallback for browsers that don't support masonry */
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
gap: 0.75rem; /* Default gap, overridden by inline styles */
|
gap: 0.75rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modern browsers with masonry support */
|
|
||||||
@supports (grid-template-rows: masonry) {
|
@supports (grid-template-rows: masonry) {
|
||||||
.masonry-container {
|
.masonry-container {
|
||||||
grid-template-rows: masonry;
|
grid-template-rows: masonry;
|
||||||
@ -497,7 +463,6 @@ if (groupingFunction) {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.masonry-container {
|
.masonry-container {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(45%, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(45%, 1fr));
|
||||||
@ -518,10 +483,10 @@ if (groupingFunction) {
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure lightbox is on top */
|
/* Ensure lightbox slides are hidden when not active */
|
||||||
.masonry-gallery [x-show="open"] {
|
.lightbox-slide.hidden {
|
||||||
z-index: 9999;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Group section styling */
|
/* Group section styling */
|
||||||
@ -538,4 +503,200 @@ if (groupingFunction) {
|
|||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class MasonryGallery {
|
||||||
|
constructor(element) {
|
||||||
|
this.element = element;
|
||||||
|
this.totalImages = parseInt(element.dataset.total || "0");
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.lightboxOpen = false;
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
this.openLightbox = this.openLightbox.bind(this);
|
||||||
|
this.closeLightbox = this.closeLightbox.bind(this);
|
||||||
|
this.next = this.next.bind(this);
|
||||||
|
this.prev = this.prev.bind(this);
|
||||||
|
this.handleKeydown = this.handleKeydown.bind(this);
|
||||||
|
this.handleLightboxSwipe = this.handleLightboxSwipe.bind(this);
|
||||||
|
this.onTouchStart = this.onTouchStart.bind(this);
|
||||||
|
this.onTouchEnd = this.onTouchEnd.bind(this);
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
this.triggers = element.querySelectorAll(".masonry-trigger");
|
||||||
|
this.lightbox = element.querySelector(".product-gallery-lightbox");
|
||||||
|
this.lightboxContent = element.querySelector(".product-gallery-lightbox-content");
|
||||||
|
this.lightboxSlides = element.querySelectorAll(".lightbox-slide");
|
||||||
|
this.closeBtn = element.querySelector(".lightbox-close");
|
||||||
|
this.prevBtn = element.querySelector(".lightbox-prev");
|
||||||
|
this.nextBtn = element.querySelector(".lightbox-next");
|
||||||
|
|
||||||
|
// State
|
||||||
|
this.touchStartX = 0;
|
||||||
|
this.touchEndX = 0;
|
||||||
|
this.minSwipeDistance = 50;
|
||||||
|
this.isSwiping = false;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Add trigger listeners
|
||||||
|
this.triggers.forEach((trigger) => {
|
||||||
|
trigger.addEventListener("click", () => {
|
||||||
|
const index = parseInt(trigger.dataset.index || "0");
|
||||||
|
this.openLightbox(index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lightbox listeners
|
||||||
|
this.closeBtn?.addEventListener("click", this.closeLightbox);
|
||||||
|
this.prevBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.prev(); });
|
||||||
|
this.nextBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.next(); });
|
||||||
|
|
||||||
|
// Close on background click
|
||||||
|
this.lightbox?.addEventListener("click", (e) => {
|
||||||
|
if (e.target === this.lightbox || e.target === this.lightboxContent) {
|
||||||
|
this.closeLightbox();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard support
|
||||||
|
window.addEventListener("keydown", this.handleKeydown);
|
||||||
|
|
||||||
|
// Swipe support
|
||||||
|
this.lightboxContent?.addEventListener("touchstart", this.onTouchStart, { passive: true });
|
||||||
|
this.lightboxContent?.addEventListener("touchend", this.onTouchEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchStart(e) {
|
||||||
|
this.touchStartX = e.changedTouches[0].screenX;
|
||||||
|
this.isSwiping = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTouchEnd(e) {
|
||||||
|
this.touchEndX = e.changedTouches[0].screenX;
|
||||||
|
this.handleLightboxSwipe();
|
||||||
|
this.isSwiping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLightboxSwipe() {
|
||||||
|
if (!this.isSwiping) return;
|
||||||
|
const swipeDistance = this.touchEndX - this.touchStartX;
|
||||||
|
if (Math.abs(swipeDistance) >= this.minSwipeDistance) {
|
||||||
|
if (swipeDistance > 0) this.prev();
|
||||||
|
else this.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openLightbox(index) {
|
||||||
|
if (index < 0 || index >= this.totalImages) return;
|
||||||
|
this.currentIndex = index;
|
||||||
|
this.lightboxOpen = true;
|
||||||
|
|
||||||
|
// Simpler visibility toggle
|
||||||
|
this.lightbox.style.display = 'flex';
|
||||||
|
this.lightbox.classList.remove('hidden');
|
||||||
|
// Force regular reflow
|
||||||
|
void this.lightbox.offsetWidth;
|
||||||
|
this.lightbox.classList.remove('opacity-0');
|
||||||
|
this.lightbox.style.opacity = '1';
|
||||||
|
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeLightbox() {
|
||||||
|
this.lightboxOpen = false;
|
||||||
|
this.lightbox.classList.add("opacity-0");
|
||||||
|
this.lightbox.style.opacity = '0';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.lightbox.style.display = 'none';
|
||||||
|
this.lightbox.classList.add("hidden");
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
// Update slides
|
||||||
|
this.lightboxSlides.forEach((slide, idx) => {
|
||||||
|
if (idx === this.currentIndex) {
|
||||||
|
slide.classList.remove("hidden");
|
||||||
|
slide.classList.remove("opacity-0");
|
||||||
|
slide.classList.add("opacity-100");
|
||||||
|
slide.classList.add("z-10");
|
||||||
|
slide.classList.remove("z-0");
|
||||||
|
} else {
|
||||||
|
slide.classList.add("opacity-0");
|
||||||
|
slide.classList.add("z-0");
|
||||||
|
slide.classList.remove("opacity-100");
|
||||||
|
slide.classList.remove("z-10");
|
||||||
|
// Delay hiding for transition ? No, just hide immediate for now or wait
|
||||||
|
// Simple visibility logic:
|
||||||
|
if (!slide.classList.contains("hidden")) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (idx !== this.currentIndex) slide.classList.add("hidden");
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update buttons visibility
|
||||||
|
if (this.prevBtn) this.prevBtn.style.display = this.currentIndex > 0 ? 'block' : 'none';
|
||||||
|
if (this.nextBtn) this.nextBtn.style.display = this.currentIndex < this.totalImages - 1 ? 'block' : 'none';
|
||||||
|
|
||||||
|
// Fix for stacked slides mess: ensure only current is visible immediately if needed, but we wanted fade.
|
||||||
|
// Better approach for simple fade:
|
||||||
|
this.lightboxSlides.forEach((slide, idx) => {
|
||||||
|
if (idx === this.currentIndex) {
|
||||||
|
slide.classList.remove('hidden');
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
slide.classList.remove('opacity-0');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
slide.classList.add('opacity-0');
|
||||||
|
// we utilize transitionend or timeout in a more complex setup, but here just creating a simple crossfade might be tricky without absolute positioning stack.
|
||||||
|
// They are absolute positioned.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next() {
|
||||||
|
if (this.currentIndex < this.totalImages - 1) {
|
||||||
|
this.currentIndex++;
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev() {
|
||||||
|
if (this.currentIndex > 0) {
|
||||||
|
this.currentIndex--;
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeydown(e) {
|
||||||
|
if (!this.lightboxOpen) return;
|
||||||
|
if (e.key === "Escape") this.closeLightbox();
|
||||||
|
if (e.key === "ArrowLeft") this.prev();
|
||||||
|
if (e.key === "ArrowRight") this.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init function avoids duplicate initialization
|
||||||
|
function initMasonryGalleries() {
|
||||||
|
document.querySelectorAll(".masonry-gallery").forEach((el) => {
|
||||||
|
if (!el.dataset.initialized) {
|
||||||
|
new MasonryGallery(el);
|
||||||
|
el.dataset.initialized = "true";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run on load and after Astro swaps
|
||||||
|
initMasonryGalleries();
|
||||||
|
document.addEventListener("astro:page-load", initMasonryGalleries);
|
||||||
|
document.addEventListener("astro:after-swap", initMasonryGalleries);
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue
Block a user