<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"
    }
  ]
}
  • Content:
    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;
    
  • URL: /components/raw/product-media-gallery/ProductMediaGallery.js
  • Filesystem Path: src/components/product-media-gallery/ProductMediaGallery.js
  • Size: 6.2 KB
  • Content:
    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);
            });
        }
    });
    
  • URL: /components/raw/product-media-gallery/index.js
  • Filesystem Path: src/components/product-media-gallery/index.js
  • Size: 2 KB
  • Content:
    $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;
    }
  • URL: /components/raw/product-media-gallery/product-media-gallery.scss
  • Filesystem Path: src/components/product-media-gallery/product-media-gallery.scss
  • Size: 5 KB

No notes defined.