| | --- |
| | |
| | import ResponsiveImage from "./ResponsiveImage.astro"; |
| |
|
| | interface ImageItem { |
| | |
| | src: any; |
| | |
| | alt: string; |
| | |
| | caption?: string; |
| | |
| | id?: string; |
| | |
| | zoomable?: boolean; |
| | |
| | downloadable?: boolean; |
| | } |
| |
|
| | interface Props { |
| | |
| | images: ImageItem[]; |
| | |
| | caption?: string; |
| | |
| | layout?: "2-column" | "3-column" | "4-column" | "auto"; |
| | |
| | zoomable?: boolean; |
| | |
| | downloadable?: boolean; |
| | |
| | class?: string; |
| | |
| | id?: string; |
| | } |
| |
|
| | const { |
| | images, |
| | caption, |
| | layout = "3-column", |
| | zoomable = false, |
| | downloadable = false, |
| | class: className, |
| | id, |
| | } = Astro.props as Props; |
| |
|
| | const hasCaptionSlot = Astro.slots.has("caption"); |
| | const hasCaption = |
| | hasCaptionSlot || (typeof caption === "string" && caption.length > 0); |
| | const uid = `mi_${Math.random().toString(36).slice(2)}`; |
| |
|
| | |
| | const getGridColumns = () => { |
| | switch (layout) { |
| | case "2-column": |
| | return "repeat(2, 1fr)"; |
| | case "3-column": |
| | return "repeat(3, 1fr)"; |
| | case "4-column": |
| | return "repeat(4, 1fr)"; |
| | case "auto": |
| | return "repeat(auto-fit, minmax(200px, 1fr))"; |
| | default: |
| | return "repeat(3, 1fr)"; |
| | } |
| | }; |
| |
|
| | const gridColumns = getGridColumns(); |
| | --- |
| |
|
| | <div |
| | class={`multi-image ${className || ""}`} |
| | data-mi-root={uid} |
| | data-layout={layout} |
| | {id} |
| | > |
| | { |
| | hasCaption ? ( |
| | <figure class="multi-image-figure"> |
| | <div |
| | class="multi-image-grid" |
| | style={`grid-template-columns: ${gridColumns}`} |
| | > |
| | {images.map((image, index) => ( |
| | <div class="multi-image-item"> |
| | <ResponsiveImage |
| | src={image.src} |
| | alt={image.alt} |
| | zoomable={image.zoomable ?? zoomable} |
| | downloadable={ |
| | image.downloadable ?? downloadable |
| | } |
| | class="multi-image-img" |
| | /> |
| | {image.caption && ( |
| | <div class="multi-image-subcaption"> |
| | {image.caption} |
| | </div> |
| | )} |
| | {image.id && ( |
| | <span |
| | id={image.id} |
| | style="position: absolute;" |
| | /> |
| | )} |
| | </div> |
| | ))} |
| | </div> |
| | <figcaption class="multi-image-caption"> |
| | {hasCaptionSlot ? ( |
| | <slot name="caption" /> |
| | ) : ( |
| | caption && <span set:html={caption} /> |
| | )} |
| | </figcaption> |
| | </figure> |
| | ) : ( |
| | <div |
| | class="multi-image-grid" |
| | style={`grid-template-columns: ${gridColumns}`} |
| | > |
| | {images.map((image, index) => ( |
| | <div class="multi-image-item"> |
| | <ResponsiveImage |
| | src={image.src} |
| | alt={image.alt} |
| | zoomable={image.zoomable ?? zoomable} |
| | downloadable={image.downloadable ?? downloadable} |
| | class="multi-image-img" |
| | /> |
| | {image.caption && ( |
| | <div class="multi-image-subcaption"> |
| | {image.caption} |
| | </div> |
| | )} |
| | {image.id && ( |
| | <span id={image.id} style="position: absolute;" /> |
| | )} |
| | </div> |
| | ))} |
| | </div> |
| | ) |
| | } |
| | </div> |
| |
|
| | <style> |
| | .multi-image { |
| | margin: var(--block-spacing-y) 0; |
| | } |
| |
|
| | .multi-image-figure { |
| | margin: 0; |
| | } |
| |
|
| | .multi-image-grid { |
| | display: grid; |
| | gap: 1rem; |
| | align-items: start; |
| | } |
| |
|
| | .multi-image-item { |
| | display: flex; |
| | flex-direction: column; |
| | text-align: center; |
| | position: relative; |
| | z-index: var(--z-content); |
| | transition: z-index 0.3s ease; |
| | } |
| |
|
| | |
| | :global(.medium-zoom--opened) .multi-image-item { |
| | opacity: 0; |
| | z-index: calc(var(--z-base) - 1); |
| | transition: |
| | opacity 0.3s ease, |
| | z-index 0.3s ease; |
| | } |
| |
|
| | |
| | :global(.medium-zoom--opened) |
| | .multi-image-item:has(:global(.medium-zoom--opened)) { |
| | opacity: 1; |
| | z-index: var(--z-overlay); |
| | } |
| |
|
| | |
| | :global(.medium-zoom--opened) .multi-image-item.zoom-active { |
| | opacity: 1 !important; |
| | z-index: var(--z-overlay) !important; |
| | } |
| |
|
| | .multi-image-item :global(.ri-root) { |
| | margin: 0; |
| | } |
| |
|
| | .multi-image-item :global(figure) { |
| | margin: 0; |
| | } |
| |
|
| | .multi-image-img { |
| | width: 100%; |
| | height: auto; |
| | object-fit: contain; |
| | } |
| |
|
| | .multi-image-subcaption { |
| | font-size: 0.85rem; |
| | color: var(--muted-color); |
| | margin-top: 0.5rem; |
| | line-height: 1.4; |
| | } |
| |
|
| | .multi-image-caption { |
| | text-align: left; |
| | font-size: 0.9rem; |
| | color: var(--muted-color); |
| | margin-top: 1rem; |
| | line-height: 1.4; |
| | } |
| |
|
| | |
| | @media (max-width: 768px) { |
| | .multi-image-grid[style*="repeat(3, 1fr)"], |
| | .multi-image-grid[style*="repeat(4, 1fr)"] { |
| | grid-template-columns: 1fr !important; |
| | gap: 1.5rem; |
| | } |
| |
|
| | .multi-image-grid[style*="repeat(2, 1fr)"] { |
| | grid-template-columns: 1fr !important; |
| | gap: 1.5rem; |
| | } |
| | } |
| |
|
| | @media (min-width: 769px) and (max-width: 1024px) { |
| | .multi-image-grid[style*="repeat(4, 1fr)"] { |
| | grid-template-columns: repeat(2, 1fr) !important; |
| | } |
| | } |
| |
|
| | |
| | .multi-image[data-layout*="column"] .multi-image-item :global(img) { |
| | height: 200px; |
| | object-fit: contain; |
| | } |
| |
|
| | |
| | .multi-image[data-layout="auto"] .multi-image-item :global(img) { |
| | height: auto; |
| | } |
| |
|
| | |
| | .multi-image-item :global(img) { |
| | max-width: 100%; |
| | display: block; |
| | margin: 0 auto; |
| | } |
| | </style> |
| |
|
| | <script> |
| | |
| | document.addEventListener("DOMContentLoaded", () => { |
| | |
| | const multiImages = document.querySelectorAll(".multi-image"); |
| |
|
| | multiImages.forEach((multiImage) => { |
| | const items = multiImage.querySelectorAll(".multi-image-item"); |
| | const zoomableImages = multiImage.querySelectorAll( |
| | 'img[data-zoomable="1"]', |
| | ); |
| |
|
| | zoomableImages.forEach((img) => { |
| | img.addEventListener("click", () => { |
| | |
| | const activeItem = img.closest(".multi-image-item"); |
| | const riRoot = img.closest(".ri-root"); |
| |
|
| | |
| | document |
| | .querySelectorAll( |
| | ".multi-image-item.zoom-active, .ri-root.zoom-active", |
| | ) |
| | .forEach((el) => el.classList.remove("zoom-active")); |
| |
|
| | |
| | if (activeItem) { |
| | activeItem.classList.add("zoom-active"); |
| | } |
| | if (riRoot) { |
| | riRoot.classList.add("zoom-active"); |
| | } |
| | }); |
| | }); |
| | }); |
| |
|
| | |
| | document.addEventListener("click", (e) => { |
| | if (e.target.classList.contains("medium-zoom-overlay")) { |
| | |
| | document |
| | .querySelectorAll( |
| | ".multi-image-item.zoom-active, .ri-root.zoom-active", |
| | ) |
| | .forEach((item) => item.classList.remove("zoom-active")); |
| | } |
| | }); |
| |
|
| | |
| | document.addEventListener("keydown", (e) => { |
| | if (e.key === "Escape") { |
| | document |
| | .querySelectorAll( |
| | ".multi-image-item.zoom-active, .ri-root.zoom-active", |
| | ) |
| | .forEach((item) => item.classList.remove("zoom-active")); |
| | } |
| | }); |
| | }); |
| | </script> |
| |
|