<div class="product-media-gallery" data-current-slide="0" data-init="">
<!-- Main image carousel -->
<div class="product-media-gallery__main">
<div class="swiper-container js-product-media-gallery">
<div class="swiper-wrapper">
<div class="swiper-slide product-media-gallery__slide">
<div class="product-media-gallery__image-wrapper">
<img src="/mocks/img/casa-f-principle.jpg" alt="PARASOL Zenith product image 1" class="product-media-gallery__image" draggable="false">
</div>
</div>
<div class="swiper-slide product-media-gallery__slide">
<div class="product-media-gallery__image-wrapper">
<img src="/mocks/img/kone_syv.png" alt="PARASOL Zenith product image 2" class="product-media-gallery__image" draggable="false">
</div>
</div>
<div class="swiper-slide product-media-gallery__slide">
<div class="product-media-gallery__image-wrapper">
<img src="/mocks/img/kone_auki_syv.png" alt="PARASOL Zenith product image 3" class="product-media-gallery__image" draggable="false">
</div>
</div>
<div class="swiper-slide product-media-gallery__slide">
<div class="product-media-gallery__image-wrapper">
<img src="/mocks/img/casa-f-principle.jpg" alt="PARASOL Zenith installation example" class="product-media-gallery__image" draggable="false">
</div>
</div>
<div class="swiper-slide product-media-gallery__slide">
<div class="product-media-gallery__video-wrapper">
<video class="product-media-gallery__video" controls muted>
<source src=/mocks/video/sample-portrait.mp4 type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
<!-- Navigation arrows -->
<div class="product-media-gallery__nav">
<button class="product-media-gallery__nav-button product-media-gallery__nav-button--prev swiper-button-prev js-prev-button" aria-label="Previous image" title="Previous image">
<svg class="icon " focusable="false">
<use xlink:href="#icon-slider-arrow"></use>
</svg>
</button>
<button class="product-media-gallery__nav-button product-media-gallery__nav-button--next swiper-button-next js-next-button" aria-label="Next image" title="Next image">
<svg class="icon " focusable="false">
<use xlink:href="#icon-slider-arrow"></use>
</svg>
</button>
</div>
</div>
<!-- Thumbnail carousel -->
<div class="product-media-gallery__thumbnails">
<div class="swiper-container js-product-media-gallery-thumbs">
<div class="swiper-wrapper">
<div class="swiper-slide product-media-gallery__thumbnail" aria-label="View image 0">
<div class="product-media-gallery__thumbnail-wrapper">
<img src="/mocks/img/casa-f-principle.jpg" alt="PARASOL Zenith product image 1" class="product-media-gallery__thumbnail-image">
</div>
</div>
<div class="swiper-slide product-media-gallery__thumbnail" aria-label="View image 1">
<div class="product-media-gallery__thumbnail-wrapper">
<img src="/mocks/img/kone_syv.png" alt="PARASOL Zenith product image 2" class="product-media-gallery__thumbnail-image">
</div>
</div>
<div class="swiper-slide product-media-gallery__thumbnail" aria-label="View image 2">
<div class="product-media-gallery__thumbnail-wrapper">
<img src="/mocks/img/kone_auki_syv.png" alt="PARASOL Zenith product image 3" class="product-media-gallery__thumbnail-image">
</div>
</div>
<div class="swiper-slide product-media-gallery__thumbnail" aria-label="View image 3">
<div class="product-media-gallery__thumbnail-wrapper">
<img src="/mocks/img/casa-f-principle.jpg" alt="PARASOL Zenith installation example" class="product-media-gallery__thumbnail-image">
</div>
</div>
<div class="swiper-slide product-media-gallery__thumbnail" aria-label="View image 4">
<div class="product-media-gallery__thumbnail-wrapper product-media-gallery__thumbnail-wrapper--video">
<img src="/mocks/img/casa-f-principle.jpg" alt="" class="product-media-gallery__thumbnail-image">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="product-media-gallery" data-current-slide="{{initialSlide}}" data-init="{{initType}}">
<!-- Main image carousel -->
<div class="product-media-gallery__main">
<div class="swiper-container js-product-media-gallery">
<div class="swiper-wrapper">
{{#each slides}}
<div class="swiper-slide product-media-gallery__slide">
{{#if video}}
<div class="product-media-gallery__video-wrapper">
<video class="product-media-gallery__video" controls muted>
<source src={{video}} type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
{{else}}
<div class="product-media-gallery__image-wrapper">
<img src="{{image}}" alt="{{alt}}" class="product-media-gallery__image" draggable="false">
</div>
{{/if}}
</div>
{{/each}}
</div>
</div>
<!-- Navigation arrows -->
<div class="product-media-gallery__nav">
<button class="product-media-gallery__nav-button product-media-gallery__nav-button--prev swiper-button-prev js-prev-button" aria-label="Previous image" title="Previous image">
{{> @icon id="slider-arrow"}}
</button>
<button class="product-media-gallery__nav-button product-media-gallery__nav-button--next swiper-button-next js-next-button" aria-label="Next image" title="Next image">
{{> @icon id="slider-arrow"}}
</button>
</div>
</div>
<!-- Thumbnail carousel -->
<div class="product-media-gallery__thumbnails">
<div class="swiper-container js-product-media-gallery-thumbs">
<div class="swiper-wrapper">
{{#each slides}}
<div class="swiper-slide product-media-gallery__thumbnail" aria-label="View image {{@index}}">
<div class="product-media-gallery__thumbnail-wrapper{{#if video}} product-media-gallery__thumbnail-wrapper--video{{/if}}">
<img src="{{thumbnailImage}}" alt="{{alt}}" class="product-media-gallery__thumbnail-image">
</div>
</div>
{{/each}}
</div>
</div>
</div>
</div>
{
"initialSlide": 0,
"slides": [
{
"image": "/mocks/img/casa-f-principle.jpg",
"thumbnailImage": "/mocks/img/casa-f-principle.jpg",
"alt": "PARASOL Zenith product image 1"
},
{
"image": "/mocks/img/kone_syv.png",
"thumbnailImage": "/mocks/img/kone_syv.png",
"alt": "PARASOL Zenith product image 2"
},
{
"image": "/mocks/img/kone_auki_syv.png",
"thumbnailImage": "/mocks/img/kone_auki_syv.png",
"alt": "PARASOL Zenith product image 3"
},
{
"image": "/mocks/img/casa-f-principle.jpg",
"thumbnailImage": "/mocks/img/casa-f-principle.jpg",
"alt": "PARASOL Zenith installation example"
},
{
"video": "/mocks/video/sample-portrait.mp4",
"image": "/mocks/img/casa-f-principle.jpg",
"thumbnailImage": "/mocks/img/casa-f-principle.jpg",
"thumbnailTitle": "Gold PX",
"thumbnailDescr": "Amet, consectetur"
}
]
}
import Swiper from 'swiper';
class ProductMediaGallery {
constructor(el, options = {}) {
this.el = el;
this.mainCarouselSelector = this.el.querySelector('.js-product-media-gallery');
this.thumbsCarouselSelector = this.el.querySelector('.js-product-media-gallery-thumbs');
// Navigation buttons
this.prevButton = this.el.querySelector('.js-prev-button');
this.nextButton = this.el.querySelector('.js-next-button');
this.thumbsPrevButton = this.el.querySelector('.js-thumbs-prev-button');
this.thumbsNextButton = this.el.querySelector('.js-thumbs-next-button');
// Current slide from data attribute or options
this.currentSlide = options.currentSlide || parseInt(this.el.dataset.currentSlide) || 0;
this.init();
}
init() {
// Initialize product media gallery carousel first
this.thumbsCarousel = new Swiper(this.thumbsCarouselSelector, {
slidesPerView: 'auto',
spaceBetween: 16,
watchSlidesProgress: true,
centeredSlides: false,
navigation: {
nextEl: this.thumbsNextButton,
prevEl: this.thumbsPrevButton
},
on: {
init: () => {
this.updateThumbnailNavigation();
},
slideChange: () => {
this.updateThumbnailNavigation();
}
}
});
// Initialize main carousel and sync with thumbnails
this.mainCarousel = new Swiper(this.mainCarouselSelector, {
spaceBetween: 10,
initialSlide: this.currentSlide,
navigation: {
nextEl: this.nextButton,
prevEl: this.prevButton
},
thumbs: {
swiper: this.thumbsCarousel
},
keyboard: {
enabled: true,
onlyInViewport: true
},
a11y: {
prevSlideMessage: (this.prevButton && this.prevButton.getAttribute('aria-label')) || 'Föregående bild',
nextSlideMessage: (this.nextButton && this.nextButton.getAttribute('aria-label')) || 'Nästa bild'
},
on: {
init: () => {
this.updateNavigationButtons();
this.dispatchCurrentSlide();
},
slideChange: () => {
this.updateNavigationButtons();
this.dispatchCurrentSlide();
}
}
});
this.attachEventListeners();
// Update to current slide for both carousels
this.mainCarousel.slideTo(this.currentSlide, 0, false);
this.thumbsCarousel.slideTo(this.currentSlide, 0, false);
this.thumbsCarousel.update();
this.mainCarousel.update();
this.updateNavigationButtons();
this.updateThumbnailNavigation();
}
// Dispatchar currentSlide-event på galleriets root-element
dispatchCurrentSlide() {
if (!this.el || !this.mainCarousel) return;
const activeIndex = this.mainCarousel.activeIndex != null ? this.mainCarousel.activeIndex : 0;
this.el.dispatchEvent(new CustomEvent('currentSlide', { detail: { slide: activeIndex } }));
}
attachEventListeners() {
// Keyboard navigation on thumbnails
const thumbnails = this.thumbsCarouselSelector.querySelectorAll('.product-media-gallery__thumbnail');
thumbnails.forEach((thumb, index) => {
thumb.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.mainCarousel.slideTo(index);
}
});
});
}
updateNavigationButtons() {
if (!this.mainCarousel) return;
const isFirst = this.mainCarousel.isBeginning;
const isLast = this.mainCarousel.isEnd;
// Hide/show prev button
if (this.prevButton) {
if (isFirst) {
this.prevButton.style.display = 'none';
this.prevButton.disabled = true;
} else {
this.prevButton.style.display = 'flex';
this.prevButton.disabled = false;
}
}
// Hide/show next button
if (this.nextButton) {
if (isLast) {
this.nextButton.style.display = 'none';
this.nextButton.disabled = true;
} else {
this.nextButton.style.display = 'flex';
this.nextButton.disabled = false;
}
}
}
updateThumbnailNavigation() {
if (!this.thumbsCarousel) return;
const isFirst = this.thumbsCarousel.isBeginning;
const isLast = this.thumbsCarousel.isEnd;
// Hide/show thumbnail prev button
if (this.thumbsPrevButton) {
if (isFirst) {
this.thumbsPrevButton.style.display = 'none';
this.thumbsPrevButton.disabled = true;
} else {
this.thumbsPrevButton.style.display = 'flex';
this.thumbsPrevButton.disabled = false;
}
}
// Hide/show thumbnail next button
if (this.thumbsNextButton) {
if (isLast) {
this.thumbsNextButton.style.display = 'none';
this.thumbsNextButton.disabled = true;
} else {
this.thumbsNextButton.style.display = 'flex';
this.thumbsNextButton.disabled = false;
}
}
}
goToSlide(index) {
if (this.mainCarousel) {
console.log('Going to slide:', index);
this.mainCarousel.slideTo(index);
this.updateNavigationButtons();
this.updateThumbnailNavigation();
}
}
destroy() {
// Destroy carousels
if (this.mainCarousel) {
this.mainCarousel.destroy(true, true);
}
if (this.thumbsCarousel) {
this.thumbsCarousel.destroy(true, true);
}
}
}
export default ProductMediaGallery;
import ProductMediaGallery from './ProductMediaGallery';
// Initialize gallery and store instance on the element
const initializeGallery = (gallery, options = {}) => {
if (!gallery) return;
// Safety: Destroy old instance if it exists
const existingInstance = gallery.productMediaGalleryInstance;
if (existingInstance) {
existingInstance.destroy();
}
// Create and store new instance
const currentSlide = gallery.getAttribute('data-current-slide') || 0;
const galleryOptions = {
currentSlide: currentSlide,
...options
};
gallery.productMediaGalleryInstance = new ProductMediaGallery(gallery, galleryOptions);
};
// Clean up gallery instance
const destroyGallery = (gallery) => {
if (!gallery) return;
const instance = gallery.productMediaGalleryInstance;
if (!instance) return;
instance.destroy();
delete gallery.productMediaGalleryInstance;
};
document.addEventListener('DOMContentLoaded', () => {
// Initialize all galleries on page load, except modal galleries
const galleries = document.querySelectorAll('.product-media-gallery:not([data-init="modal"])');
galleries.forEach(gallery => initializeGallery(gallery));
// Only create modal listeners if modal galleries exist
const hasModalGalleries = document.querySelector('.product-media-gallery[data-init="modal"]');
if (hasModalGalleries) {
document.addEventListener('modalOpened', (e) => {
const modal = document.getElementById(e.detail.modalId);
if (!modal) return;
const gallery = modal.querySelector('.product-media-gallery[data-init="modal"]');
initializeGallery(gallery);
});
document.addEventListener('modalClosed', (e) => {
const modal = document.getElementById(e.detail.modalId);
if (!modal) return;
const gallery = modal.querySelector('.product-media-gallery[data-init="modal"]');
destroyGallery(gallery);
});
}
});
$galleryMaxHeight: 100%;
$thumbSize: 120px;
$thumbsContainerPaddingY: #{size(2)};
$thumbsContainerHeight: calc(#{$thumbSize} + 2 * #{$thumbsContainerPaddingY});
.product-media-gallery {
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
}
// Main carousel
.product-media-gallery__main {
position: relative;
width: 100%;
max-height: calc(#{$galleryMaxHeight} - #{$thumbsContainerHeight});
height: calc(#{$galleryMaxHeight} - #{$thumbsContainerHeight});
.swiper-container {
height: 100%;
}
}
.product-media-gallery__slide {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
max-height: 100%;
}
.product-media-gallery__image-wrapper {
width: 100%;
height: 100%;
max-height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
position: relative;
@include breakpoint($s) {
padding: size(2);
}
@include breakpoint($m) {
height: 700px;
max-height: 700px;
}
}
.product-media-gallery__image {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
user-select: none;
max-height: 100%;
@include breakpoint($m) {
max-height: 700px;
object-fit: contain;
}
}
.product-media-gallery__video-wrapper {
aspect-ratio: 16/9;
width: 100%;
max-height: 100%;
padding: 0 size(2);
video {
width: 100%;
height: 100%;
}
}
// Navigation arrows for main carousel
.product-media-gallery__nav {
display: flex;
align-items: center;
justify-content: space-between;
position: absolute;
top: calc(100%);
left: 0;
right: 0;
transform: translateY(-40px);
z-index: 10;
pointer-events: none;
@include breakpoint($s) {
top: calc(50%);
transform: translateY(-50%);
}
}
.product-media-gallery__nav-button {
pointer-events: all;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
cursor: pointer;
padding: 0;
top: -20px;
&:disabled,
&[style*="display: none"] {
opacity: 0;
pointer-events: none;
}
.icon {
width: 32px;
height: 32px;
fill: currentColor;
background-color: $color-white;
border-radius: 100%;
}
}
.product-media-gallery__nav-button--prev {
margin-left: size(1);
@include breakpoint($m) {
margin-left: size(2);
}
}
.product-media-gallery__nav-button--next {
margin-right: size(1);
@include breakpoint($m) {
margin-right: size(2);
}
}
// Thumbnails
.product-media-gallery__thumbnails {
position: relative;
width: 100%;
max-height: $thumbsContainerHeight;
padding: $thumbsContainerPaddingY size(2);
display: flex;
justify-content: center;
@include breakpoint($m) {
padding: $thumbsContainerPaddingY size(2);
}
.swiper-container {
max-width: 100%;
}
}
.product-media-gallery__thumbnail {
cursor: pointer;
flex-shrink: 0;
width: $thumbSize !important;
&.swiper-slide-thumb-active {
.product-media-gallery__thumbnail-wrapper {
border-color: $color-green;
}
}
}
.product-media-gallery__thumbnail-wrapper {
position: relative;
width: $thumbSize;
height: $thumbSize;
border: 2px solid $color-gray-2;
overflow: hidden;
transition: border-color 0.3s ease;
background: $color-gray-1;
&--video {
&::before {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
content: "";
border: 1px solid transparent;
z-index: 1;
background-color: black;
opacity: 0.4;
}
&::after {
content: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAyMyAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExLjQ0NzMgMjIuODA3MkMxNy43MTEzIDIyLjgwNzIgMjIuNzg5MiAxNy44MjQ5IDIyLjc4OTIgMTEuNjc5QzIyLjc4OTIgNS41MzMwNSAxNy43MTEzIDAuNTUwNzgxIDExLjQ0NzMgMC41NTA3ODFDNS4xODMzOSAwLjU1MDc4MSAwLjEwNTQ2OSA1LjUzMzA1IDAuMTA1NDY5IDExLjY3OUMwLjEwNTQ2OSAxNy44MjQ5IDUuMTgzMzkgMjIuODA3MiAxMS40NDczIDIyLjgwNzJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOS4xNzk2OSA3LjIyNjU2TDE1Ljk4NDggMTEuNjc3OEw5LjE3OTY5IDE2LjEyOTFWNy4yMjY1NloiIGZpbGw9IiM0MzQzNDMiLz4KPC9zdmc+Cg==");
position: absolute;
bottom: calc(50% - 12px);
left: calc(50% - 12px);
width: 24px;
height: 24px;
z-index: 1;
}
}
}
.product-media-gallery__thumbnail-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
No notes defined.