Videoslider

100%
<div class="cell">
    <section class="grid-container o-section">
        <div class="grid-x grid-margin-x">
            <div class="vs-container" data-href="/assets/json/videos.json">
                <aside class="vs-sidebar">
                    <h2>Short News</h2>
                    <p>Kurze Video Beiträge zu aktuellen Themen</p>
                </aside>
                <div class="vs-filters" role="region" aria-label="Filter-Leiste">
                    <ul class="vs-context-menu__list">
                        <li class="vs-context-menu__item">
                            <button class="vs-filter-btn vs-context-menu__link active" data-filter="all" aria-label="Filter: Alle" aria-pressed="true">Alle Beiträge
              </button>
                        </li>
                        <li class="vs-context-menu__item">
                            <button class="vs-filter-btn vs-context-menu__link" data-filter="459" aria-label="Filter: Gastro" aria-pressed="false">Gastro
              </button>
                        </li>
                        <li class="vs-context-menu__item">
                            <button class="vs-filter-btn vs-context-menu__link" data-filter="458" aria-label="Filter: Hildesheim" aria-pressed="false">Hildesheim
              </button>
                        </li>
                    </ul>
                </div>
                <div class="vs-slider-area">
                    <div class="vs-class-slider-grid-wrapper vs-slider-grid swiper">
                        <div class="swiper-wrapper">
                            <f:for each="{items}" as="video" iteration="i">
                                <div class="swiper-slide vs-slide" data-slide-index="{i.index}" data-open-modal="{i.index}">
                                    <div class="vs-thumbnail vs-thumbnail-index-{i.index}">
                                        <img src="{video.resolvedThumbnail}" alt="{video.title}">
                                        <span class="vs-play-icon"></span>
                                        <div class="vs-thumb-overlay">
                                            <div class="vs-thumb-text">
                                                <span class="c-badge c-badge--user"><strong>HAZ+</strong></span>
                                                <span class="vs-category" data-id="{video.category.id}">{video.category.title}</span>
                                                <h2>{video.title}</h2>
                                            </div>
                                            <span>{video.shortdesc}</span>
                                        </div>
                                    </div>
                                </div>
                            </f:for>
                        </div>
                        <div class="swiper-button-prev vs-nav-arrow vs-nav-prev" aria-label="Vorheriges Slide"></div>
                        <div class="swiper-button-next vs-nav-arrow vs-nav-next" aria-label="Nächstes Slide"></div>
                    </div>
                    <div class="vs-video-modal vs-slider-grid close">
                        <div class="vs-modal-content">
                            <div class="vs-modal-video-wrapper">
                                <div class="vs-modal-video-inner"></div>
                            </div>
                            <div class="vs-modal-text">
                                <div class="vs-modal-title vs-thumb-text">
                                    <span class="c-badge c-badge--user"><strong>HAZ+</strong></span>
                                    <span class="vs-category">{currentCategory}</span>
                                    <h2>{currentTitle}</h2>
                                </div>
                                <p class="vs-modal-desc">{currentDescription}</p>
                                <a href="" class="vs-modal-cta vs-cta-btn">Mehr erfahren</a>
                                <button class="vs-close-modal" aria-label="Schließen">×</button>
                            </div>
                        </div>
                        <button class="vs-slider-nav vs-modal-nav vs-modal-prev" aria-label="Vorheriges Video">&lt;</button>
                        <button class="vs-slider-nav vs-modal-nav vs-modal-next" aria-label="Nächstes Video">&gt;</button>
                    </div>
                </div>
            </div>
        </div>
    </section>
</div>
<div class="cell">
  <section class="grid-container o-section">
    <div class="grid-x grid-margin-x">
      <div class="vs-container" data-href="/assets/json/videos.json">
        <aside class="vs-sidebar">
          <h2>Short News</h2>
          <p>Kurze Video Beiträge zu aktuellen Themen</p>
        </aside>
        <div class="vs-filters" role="region" aria-label="Filter-Leiste">
          <ul class="vs-context-menu__list">
            <li class="vs-context-menu__item">
              <button class="vs-filter-btn vs-context-menu__link active" data-filter="all" aria-label="Filter: Alle"
                      aria-pressed="true">Alle Beiträge
              </button>
            </li>
            <li class="vs-context-menu__item">
              <button class="vs-filter-btn vs-context-menu__link" data-filter="459" aria-label="Filter: Gastro"
                      aria-pressed="false">Gastro
              </button>
            </li>
            <li class="vs-context-menu__item">
              <button class="vs-filter-btn vs-context-menu__link" data-filter="458" aria-label="Filter: Hildesheim"
                      aria-pressed="false">Hildesheim
              </button>
            </li>
          </ul>
        </div>
        <div class="vs-slider-area">
          <div class="vs-class-slider-grid-wrapper vs-slider-grid swiper">
            <div class="swiper-wrapper">
              <f:for each="{items}" as="video" iteration="i">
                <div class="swiper-slide vs-slide" data-slide-index="{i.index}" data-open-modal="{i.index}">
                  <div class="vs-thumbnail vs-thumbnail-index-{i.index}">
                    <img src="{video.resolvedThumbnail}" alt="{video.title}">
                    <span class="vs-play-icon"></span>
                    <div class="vs-thumb-overlay">
                      <div class="vs-thumb-text">
                        <span class="c-badge c-badge--user"><strong>HAZ+</strong></span>
                        <span class="vs-category" data-id="{video.category.id}">{video.category.title}</span>
                        <h2>{video.title}</h2>
                      </div>
                      <span>{video.shortdesc}</span>
                    </div>
                  </div>
                </div>
              </f:for>
            </div>
            <div class="swiper-button-prev vs-nav-arrow vs-nav-prev" aria-label="Vorheriges Slide"></div>
            <div class="swiper-button-next vs-nav-arrow vs-nav-next" aria-label="Nächstes Slide"></div>
          </div>
          <div class="vs-video-modal vs-slider-grid close">
            <div class="vs-modal-content">
              <div class="vs-modal-video-wrapper">
                <div class="vs-modal-video-inner"></div>
              </div>
              <div class="vs-modal-text">
                <div class="vs-modal-title vs-thumb-text">
                  <span class="c-badge c-badge--user"><strong>HAZ+</strong></span>
                  <span class="vs-category">{currentCategory}</span>
                  <h2>{currentTitle}</h2>
                </div>
                <p class="vs-modal-desc">{currentDescription}</p>
                <a href="" class="vs-modal-cta vs-cta-btn">Mehr erfahren</a>
                <button class="vs-close-modal" aria-label="Schließen">×</button>
              </div>
            </div>
            <button class="vs-slider-nav vs-modal-nav vs-modal-prev" aria-label="Vorheriges Video">&lt;</button>
            <button class="vs-slider-nav vs-modal-nav vs-modal-next" aria-label="Nächstes Video">&gt;</button>
          </div>
        </div>
      </div>
    </div>
  </section>
</div>
/* No context defined for this component. */
  • Content:
    import Swiper from 'swiper/swiper-bundle';
    
    class Videoslider {
      /**
       *
       * @param {HTMLElement} element
       */
      constructor(element) {
        this.swiperContainer = element.querySelector('.vs-class-slider-grid-wrapper.swiper');
        this.swiperWrapper = this.swiperContainer.querySelector('.swiper-wrapper');
        this.swiperPrev = this.swiperContainer.querySelector('.vs-nav-prev');
        this.swiperNext = this.swiperContainer.querySelector('.vs-nav-next');
        this.current = 0;
    
        this.container = element;
        this.container.classList.add('loading');
    
        this.videos = [];
        this.filteredVideos = [];
    
        this.modal = element.querySelector('.vs-video-modal');
        this.modal.setAttribute('role', 'dialog');
        this.modal.setAttribute('aria-modal', 'true');
        this.modal.setAttribute('aria-labelledby', 'modalTitle');
        this.modal.setAttribute('aria-describedby', 'modalDesc');
        this.titleEl = this.modal.querySelector('.vs-modal-title');
        this.descEl = this.modal.querySelector('.vs-modal-desc');
        this.ctaEl = this.modal.querySelector('.vs-modal-cta');
        this.videoWrapper = this.modal.querySelector('.vs-modal-video-wrapper');
        this.videoWrapperInner = this.videoWrapper.querySelector('.vs-modal-video-inner');
    
        this.registerModalEvents();
        this.registerFilterEvents(element);
        // Globale Referenz für Event-Delegation
        window.videoslider = this;
      }
    
      initialize(response) {
        response
          // eslint-disable-next-line no-shadow
          .then((response) => response.json())
          // eslint-disable-next-line no-shadow
          .then((response) => {
            this.videos = response;
            this.filteredVideos = this.videos;
            this.renderSlides(this.videos);
          })
          .catch((error) => console.error('Fehler beim Laden der JSON-Daten:', error));
      }
    
      renderSlides(items) {
        // Always destroy existing swiper before rendering new slides
        if (this.swiper) {
          this.swiper.destroy(true, true);
          this.swiper = null;
        }
        this.filteredVideos = items;
        this.swiperWrapper.innerHTML = '';
        items.forEach((video, index) => {
          const slide = this.createSlide(video, index);
          this.swiperWrapper.appendChild(slide);
          slide.setAttribute('role', 'group');
          slide.setAttribute('aria-label', `Video ${index + 1} von ${items.length}`);
        });
        this.swiper = new Swiper(this.swiperContainer, {
          slidesPerView: 'auto',
          minimumVelocity: 1,
          centeredSlides: true,
          spaceBetween: 16,
          autoHeight: true,
          loop: items.length > 4,
          navigation: {
            nextEl: '.vs-nav-next',
            prevEl: '.vs-nav-prev',
          },
          breakpoints: {
            768: {
              slidesPerView: 'auto',
              centeredSlides: false,
            },
          },
        });
        this.swiper.on('click', (swiper, event) => {
          // Verhindert Klick bei größeren Swipe-Bewegungen, erlaubt kleine Verschiebungen
          if (swiper && typeof swiper.touches?.diff === 'number' && Math.abs(swiper.touches.diff) > 20) return;
          if (!event || typeof event.target?.closest !== 'function') return;
    
          const slide = event.target.closest('.swiper-slide');
          if (!slide || !slide.dataset.slideIndex) return;
    
          const index = parseInt(slide.dataset.slideIndex, 10);
          if (!Number.isNaN(index)) {
            this.openModal(index);
          }
        });
        this.swiper.update();
        if (items.length > 4) {
          this.unlockNavigationButtons();
        }
        this.container.classList.remove('loading');
      }
    
      openModal(index) {
        const video = this.filteredVideos[index];
        if (!video) return;
        this.current = index;
        this.videoWrapperInner.innerHTML = video.videocode;
    
        this.titleEl.innerHTML = `
            ${video.hazplus ? '<span class="c-badge c-badge--user"><strong>HAZ+</strong></span>' : ''}
            <span>${video.filteredCategory?.title ?? ''}</span>
            <h2 id="modalTitle">${video.title}</h2>
          `;
    
        this.descEl.innerHTML = video.content;
        this.descEl.setAttribute('id', 'modalDesc');
        const link = video.resolvedLink || video.cta;
        if (link) {
          this.ctaEl.href = link;
          this.ctaEl.style.display = '';
        } else {
          this.ctaEl.style.display = 'none';
        }
        // Show modal
        this.modal.classList.remove('close');
        this.modal.classList.add('open');
        this.modal.setAttribute('tabindex', '-1');
        this.modal.focus();
        this.swiperPrev.style.display = 'none';
        this.swiperNext.style.display = 'none';
      }
    
      closeModal() {
        this.videoWrapperInner.innerHTML = '';
        this.modal.classList.remove('open');
        this.modal.classList.add('close');
        this.swiperPrev.style.display = 'flex';
        this.swiperNext.style.display = 'flex';
      }
    
      registerModalEvents() {
        const modalPrev = this.modal.querySelector('.vs-modal-prev');
        const modalNext = this.modal.querySelector('.vs-modal-next');
        const modalClose = this.modal.querySelector('.vs-close-modal');
    
        modalPrev.addEventListener('click', () => {
          this.closeModal();
          // eslint-disable-next-line max-len
          const prevIndex = (this.current - 1 + this.filteredVideos.length) % this.filteredVideos.length;
          this.openModal(prevIndex);
        });
        modalNext.addEventListener('click', () => {
          this.closeModal();
          const nextIndex = (this.current + 1) % this.filteredVideos.length;
          this.openModal(nextIndex);
        });
        modalClose.setAttribute('role', 'button');
        modalClose.setAttribute('tabindex', '0');
        modalClose.setAttribute('aria-label', 'Modal schließen');
        modalClose.addEventListener('click', () => {
          this.closeModal();
        });
        modalClose.addEventListener('keydown', (e) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            this.closeModal();
          }
        });
        document.addEventListener('keydown', (e) => {
          if (e.key === 'Escape' && this.modal.classList.contains('open')) {
            this.closeModal();
          }
        });
      }
    
      registerFilterEvents(element) {
        const filterBtns = element.querySelectorAll('.vs-filter-btn');
        filterBtns.forEach((btn) => {
          btn.setAttribute('role', 'button');
          btn.setAttribute('tabindex', '0');
          btn.setAttribute('aria-pressed', btn.classList.contains('active'));
          btn.addEventListener('click', () => {
            filterBtns.forEach((b) => b.classList.remove('active'));
            btn.classList.add('active');
            filterBtns.forEach((b) => b.setAttribute('aria-pressed', 'false'));
            btn.setAttribute('aria-pressed', 'true');
    
            if (btn.dataset.filter === 'all') {
              if (this.swiper) {
                this.swiper.destroy(true, true);
                this.swiper = null;
              }
    
              this.swiperWrapper.innerHTML = '';
              this.swiperContainer.classList.remove('swiper-initialized', 'swiper-backface-hidden');
              this.renderSlides(this.videos);
              return;
            }
    
            const selectedFilter = btn.dataset.filter;
            const slides = this.videos.filter((video) => {
              if (!video.filteredCategory || !video.filteredCategory.id) return false;
              return String(video.filteredCategory.id) === selectedFilter;
            });
            this.renderSlides(slides);
          });
        });
      }
    
      // eslint-disable-next-line class-methods-use-this
      createSlide(video, index) {
        const slide = document.createElement('div');
        slide.dataset.slideIndex = index;
        slide.className = 'swiper-slide vs-slide';
        slide.innerHTML = `
        <div class="vs-thumbnail vs-thumbnail-index-${index}">
          <img src="${video.resolvedThumbnail}" alt="${video.title}" style="width:100%;height:100%;object-fit:cover;">
          <span class="vs-play-icon">&#9658;</span>
          <div class="vs-thumb-overlay">
            <div class="vs-thumb-text">
              ${video.hazplus ? '<span class="c-badge c-badge--user"><strong>HAZ+</strong></span>' : ''}
              <span class="vs-category" data-id="${video.filteredCategory?.id ?? ''}">${video.filteredCategory?.title ?? ''}</span>
              <h2>${video.title}</h2>
            </div>
            <span>${video.shortdesc}</span>
          </div>
        </div>
      `;
        return slide;
      }
    
      unlockNavigationButtons() {
        this.swiperPrev.classList.remove('swiper-button-lock');
        this.swiperNext.classList.remove('swiper-button-lock');
      }
    }
    
    export default Videoslider;
    
  • URL: /components/raw/videoslider/videoslider.js
  • Filesystem Path: src/patterns/20-components/videoslider/videoslider.js
  • Size: 8.3 KB

There are no notes for this item.