<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"><</button>
<button class="vs-slider-nav vs-modal-nav vs-modal-next" aria-label="Nächstes Video">></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"><</button>
<button class="vs-slider-nav vs-modal-nav vs-modal-next" aria-label="Nächstes Video">></button>
</div>
</div>
</div>
</div>
</section>
</div>
/* No context defined for this component. */
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">►</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;
There are no notes for this item.