PageSourceSearch

https://www.buhl.de/wp-content/themes/wp-themes-buhl-cobe-theme/mo…/content-slides.js?ver=1790144012

js buhl.de collected 2026-09-24 09:10:47 UTC 21,942 bytes, 621 lines download raw bytes

1/**
2 * Content-Slides Slider Manager
3 * 
4 * Funktionalität:
5 * - Klick auf Pfeil verschiebt Slider um 1 Kachel
6 * - Responsive auf allen Geräten
7 * - Hard Stop bei Endpunkten
8 * - Video-Modals mit iframe Einbettung
9 */
10
11window.ContentSlidesManager = (function() {
12    'use strict';
13    
14    /**
15     * Konfiguration
16     */
17    const DEFAULT_CONFIG = {
18        autoplay: false,
19        autoplayInterval: 5000,
20        slidePerView: 3,
21        gap: 32,
22        duration: 400,
23        easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
24    };
25    
26    /**
27     * Alle aktiven Slider-Instanzen
28     */
29    const instances = new Map();
30    
31    /**
32     * Slider-Klasse
33     */
34    class Slider {
35        constructor(containerOrSelector, config = {}) {
36            this.container = containerOrSelector instanceof Element
37                ? containerOrSelector
38                : document.querySelector(containerOrSelector);
39            if (!this.container) {
40                console.error('Content-Slides Container nicht gefunden:', containerOrSelector);
41                return;
42            }
43            
44            this.config = { ...DEFAULT_CONFIG, ...config };
45            this.track = this.container.querySelector('.content-slides__track');
46            this.slides = Array.from(this.container.querySelectorAll('.content-slides__slide'));
47            this.nav = this.container.querySelector('.content-slides__nav');
48            this.prevBtn = this.container.querySelector('.content-slides__nav-btn--prev');
49            this.nextBtn = this.container.querySelector('.content-slides__nav-btn--next');
50            this.liveRegion = null;
51            
52            this.currentIndex = 0;
53            this.slideWidth = 0;
54            this.currentGap = 32; // Default, wird in calculateDimensions() aktualisiert
55            this.autoplayTimer = null;
56            
57            // Touch/Swipe Support
58            this.touchStartX = 0;
59            this.touchEndX = 0;
60            this.touchStartY = 0;
61            this.touchEndY = 0;
62            this.isSwiping = false;
63            
64            this.init();
65        }
66        
67        /**
68         * Initialisierung
69         */
70        init() {
71            if (!this.track || this.slides.length === 0) {
72                console.error('Content-Slides Track oder Slides nicht gefunden');
73                return;
74            }
75            
76            this.setupEventListeners();
77            this.calculateDimensions();
78            this.renderSlides();
79            this.setInitialSlideForViewport();
80            this.updateNavVisibility();
81            this.updateNavButtons();
82            this.setupVideoModal();
83            this.setupWindowResize();
84            
85            if (this.config.autoplay) {
86                this.startAutoplay();
87            }
88        }
89
90        /**
91         * Auf Mobile standardmäßig mit der zweiten Slide starten.
92         */
93        setInitialSlideForViewport() {
94            const isMobile = window.innerWidth <= 768;
95            const hasSecondSlide = this.slides.length > 1;
96
97            this.currentIndex = (isMobile && hasSecondSlide) ? 1 : 0;
98            this.updateTrack();
99        }
100        
101        /**
102         * Event Listeners setzen
103         */
104        setupEventListeners() {
105            if (this.prevBtn) {
106                this.prevBtn.addEventListener('click', () => this.prev());
107                this.prevBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.prev(); } });
108            }
109            if (this.nextBtn) {
110                this.nextBtn.addEventListener('click', () => this.next());
111                this.nextBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.next(); } });
112            }
113            
114            // Keyboard Navigation nur wenn Container fokussiert ist
115            this.container.setAttribute('tabindex', '0');
116            this.container.addEventListener('keydown', (e) => {
117                if (e.key === 'ArrowLeft') {
118                    e.preventDefault();
119                    this.prev();
120                } else if (e.key === 'ArrowRight') {
121                    e.preventDefault();
122                    this.next();
123                }
124            });
125            
126            // Touch/Swipe Support
127            this.track.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: true });
128            this.track.addEventListener('touchmove', (e) => this.handleTouchMove(e), { passive: false });
129            this.track.addEventListener('touchend', (e) => this.handleTouchEnd(e), { passive: true });
130        }
131        
132        /**
133         * Responsive Dimensionen berechnen
134         */
135        calculateDimensions() {
136            const containerWidth = this.container.offsetWidth;
137            const slidePerView = this.getSlidePerView();
138            
139            // Gap aus CSS lesen statt fix hardcoded
140            const computedStyle = window.getComputedStyle(this.track);
141            const gapValue = computedStyle.gap || computedStyle.columnGap || '32px';
142            const gap = parseInt(gapValue);
143            
144            const totalGap = (slidePerView - 1) * gap;
145            
146            // Padding aus CSS lesen
147            const paddingLeft = parseInt(computedStyle.paddingLeft) || 0;
148            const paddingRight = parseInt(computedStyle.paddingRight) || 0;
149            const trackPadding = paddingLeft + paddingRight;
150            
151            this.slideWidth = (containerWidth - totalGap - trackPadding) / slidePerView;
152            this.currentGap = gap;
153        }
154        
155        /**
156         * Bestimmt Anzahl sichtbare Slides pro Bildschirmgröße
157         */
158        getSlidePerView() {
159            const width = window.innerWidth;
160            
161            if (width <= 768) return 1;    // Mobile
162            if (width <= 1024) return 2;   // Tablet
163            return 3;                       // Desktop
164        }
165        
166        /**
167         * Slides rendern
168         */
169        renderSlides() {
170            this.slides.forEach((slide, index) => {
171                slide.style.minWidth = this.slideWidth + 'px';
172                slide.style.flex = `0 0 ${this.slideWidth}px`;
173            });
174        }
175        
176        /**
177         * Zum nächsten Slide
178         */
179        next() {
180            const maxIndex = this.slides.length - this.getSlidePerView();
181            
182            if (this.currentIndex < maxIndex) {
183                this.currentIndex++;
184                this.updateTrack();
185                this.updateNavButtons();
186            }
187        }
188        
189        /**
190         * Zum vorherigen Slide
191         */
192        prev() {
193            if (this.currentIndex > 0) {
194                this.currentIndex--;
195                this.updateTrack();
196                this.updateNavButtons();
197            }
198        }
199        
200        /**
201         * Track Position aktualisieren
202         */
203        updateTrack() {
204            const offset = -(this.currentIndex * (this.slideWidth + this.currentGap));
205            this.track.style.transition = `transform ${this.config.duration}ms ${this.config.easing}`;
206            this.track.style.transform = `translateX(${offset}px)`;
207            // ARIA-Update: aktiven Slide ankündigen
208            let liveRegion = this.liveRegion || this.container.querySelector('.content-slides__sr-status');
209            if (!liveRegion) {
210                liveRegion = document.createElement('div');
211                liveRegion.className = 'content-slides__sr-status';
212                liveRegion.setAttribute('aria-live', 'polite');
213                liveRegion.setAttribute('role', 'status');
214                liveRegion.style.position = 'absolute';
215                liveRegion.style.width = '1px';
216                liveRegion.style.height = '1px';
217                liveRegion.style.overflow = 'hidden';
218                liveRegion.style.clip = 'rect(1px, 1px, 1px, 1px)';
219                liveRegion.style.whiteSpace = 'nowrap';
220                this.container.appendChild(liveRegion);
221            }
222            this.liveRegion = liveRegion;
223            const total = this.slides.length;
224            const visible = this.getSlidePerView();
225            const start = this.currentIndex + 1;
226            const end = Math.min(this.currentIndex + visible, total);
227            liveRegion.textContent = `Zeige Slides ${start} bis ${end} von ${total}`;
228        }
229        
230        /**
231         * Navigation Buttons aktivieren/deaktivieren
232         */
233        updateNavButtons() {
234            const maxIndex = this.slides.length - this.getSlidePerView();
235            
236            // Prev Button
237            if (this.prevBtn) {
238                this.prevBtn.disabled = this.currentIndex === 0;
239            }
240            
241            // Next Button
242            if (this.nextBtn) {
243                this.nextBtn.disabled = this.currentIndex >= maxIndex;
244            }
245        }
246
247        /**
248         * Navigation ein-/ausblenden (nur wenn Überlauf vorhanden)
249         */
250        updateNavVisibility() {
251            if (!this.nav) {
252                return;
253            }
254
255            const hasOverflow = this.slides.length > this.getSlidePerView();
256            this.nav.style.display = hasOverflow ? '' : 'none';
257            this.nav.setAttribute('aria-hidden', hasOverflow ? 'false' : 'true');
258        }
259        
260        /**
261         * Touch Start Handler
262         */
263        handleTouchStart(e) {
264            this.touchStartX = e.touches[0].clientX;
265            this.touchStartY = e.touches[0].clientY;
266            this.isSwiping = false;
267        }
268        
269        /**
270         * Touch Move Handler
271         */
272        handleTouchMove(e) {
273            if (!this.touchStartX) {
274                return;
275            }
276            
277            const touchCurrentX = e.touches[0].clientX;
278            const touchCurrentY = e.touches[0].clientY;
279            const diffX = this.touchStartX - touchCurrentX;
280            const diffY = this.touchStartY - touchCurrentY;
281            
282            // Prüfen ob horizontaler Swipe (und nicht vertikales Scrollen)
283            if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 10) {
284                this.isSwiping = true;
285                // Verhindere vertikales Scrollen während horizontalem Swipe
286                e.preventDefault();
287            }
288        }
289        
290        /**
291         * Touch End Handler
292         */
293        handleTouchEnd(e) {
294            if (!this.isSwiping) {
295                this.touchStartX = 0;
296                this.touchStartY = 0;
297                return;
298            }
299            
300            this.touchEndX = e.changedTouches[0].clientX;
301            this.touchEndY = e.changedTouches[0].clientY;
302            
303            this.handleSwipe();
304            
305            // Reset
306            this.touchStartX = 0;
307            this.touchEndX = 0;
308            this.touchStartY = 0;
309            this.touchEndY = 0;
310            this.isSwiping = false;
311        }
312        
313        /**
314         * Swipe Geste auswerten
315         */
316        handleSwipe() {
317            const diffX = this.touchStartX - this.touchEndX;
318            const threshold = 50; // Mindest-Swipe-Distanz in px
319            
320            if (Math.abs(diffX) > threshold) {
321                if (diffX > 0) {
322                    // Swipe nach links = nächster Slide
323                    this.next();
324                } else {
325                    // Swipe nach rechts = vorheriger Slide
326                    this.prev();
327                }
328            }
329        }
330        
331        /**
332         * Video Modal Setup
333         */
334        setupVideoModal() {
335            const videoPlayBtns = this.container.querySelectorAll('.content-slides__video-play-btn');
336            
337            videoPlayBtns.forEach(btn => {
338                btn.addEventListener('click', (e) => {
339                    e.preventDefault();
340                    e.stopPropagation();
341                    
342                    const videoUrl = btn.dataset.videoUrl;
343                    this.openVideoModal(videoUrl);
344                });
345            });
346            
347            // Kachel selbst ist auch klickbar
348            const videoThumbnails = this.container.querySelectorAll('.content-slides__video-thumbnail');
349            videoThumbnails.forEach(thumbnail => {
350                thumbnail.addEventListener('click', () => {
351                    const btn = thumbnail.querySelector('.content-slides__video-play-btn');
352                    if (btn) {
353                        btn.click();
354                    }
355                });
356            });
357            const previouslyFocused = document.activeElement;
358        }
359        
360        /**
361         * Video Modal öffnen
362         */
363        openVideoModal(videoUrl) {
364            // Fokus vor Öffnen merken
365            const previouslyFocused = document.activeElement;
366
367            // Modal erstellen
368            const modal = document.createElement('div');
369            modal.className = 'content-slides__video-modal';
370            modal.innerHTML = `
371                <div class="content-slides__video-modal-overlay"></div>
372                <div class="content-slides__video-modal-content">
373                    <button class="content-slides__video-modal-close" aria-label="Schließen">×</button>
374                    <div class="content-slides__video-modal-embed">
375                        ${this.getVideoEmbed(videoUrl)}
376                    </div>
377                </div>
378            `;
379
380            document.body.appendChild(modal);
381
382            // Fokus zurücksetzen, falls möglich
383            if (previouslyFocused && previouslyFocused.focus) {
384                previouslyFocused.focus();
385            }
386
387            // Trigger Reflow für Animation
388            setTimeout(() => modal.classList.add('is-active'), 10);
389
390            // Zyklischer Focus innerhalb des Modals
391            modal.addEventListener('keydown', (e) => {
392                if (e.key === 'Tab') {
393                    if (e.shiftKey) {
394                        if (document.activeElement === firstFocusable) {
395                            e.preventDefault();
396                            lastFocusable.focus();
397                        }
398                    } else {
399                        if (document.activeElement === lastFocusable) {
400                            e.preventDefault();
401                            firstFocusable.focus();
402                        }
403                    }
404                }
405            });
406            
407            // Close Listener
408            const closeBtn = modal.querySelector('.content-slides__video-modal-close');
409            const overlay = modal.querySelector('.content-slides__video-modal-overlay');
410            
411            const closeModal = () => {
412                modal.classList.remove('is-active');
413                setTimeout(() => modal.remove(), 300);
414            };
415            
416            closeBtn.addEventListener('click', closeModal);
417            overlay.addEventListener('click', closeModal);
418            
419            // ESC zum Schließen
420            const handleEsc = (e) => {
421                if (e.key === 'Escape') {
422                    closeModal();
423                    document.removeEventListener('keydown', handleEsc);
424                }
425            };
426            document.addEventListener('keydown', handleEsc);
427        }
428        
429        /**
430         * Video Embed HTML generieren
431         */
432        getVideoEmbed(url) {
433            const width = 640;
434            const height = 360;
435            
436            // YouTube
437            if (url.includes('youtu')) {
438                const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/);
439                if (match && match[1]) {
440                    return `
441                        <iframe 
442                            width="${width}" 
443                            height="${height}" 
444                            src="https://www.youtube.com/embed/${encodeURIComponent(match[1])}" 
445                            frameborder="0" 
446                            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
447                            allowfullscreen
448                        ></iframe>
449                    `;
450                }
451            }
452            
453            // Vimeo
454            if (url.includes('vimeo')) {
455                const match = url.match(/vimeo\.com\/(\d+)/);
456                if (match && match[1]) {
457                    return `
458                        <iframe 
459                            src="https://player.vimeo.com/video/${encodeURIComponent(match[1])}" 
460                            width="${width}" 
461                            height="${height}" 
462                            frameborder="0" 
463                            allow="autoplay; fullscreen; picture-in-picture" 
464                            allowfullscreen
465                        ></iframe>
466                    `;
467                }
468            }
469            
470            // Fallback
471            return `<iframe src="${encodeURIComponent(url)}" width="${width}" height="${height}" frameborder="0" allowfullscreen></iframe>`;
472        }
473        
474        /**
475         * Window Resize Listener
476         */
477        setupWindowResize() {
478            let resizeTimeout;
479            
480            window.addEventListener('resize', () => {
481                clearTimeout(resizeTimeout);
482                resizeTimeout = setTimeout(() => {
483                    this.calculateDimensions();
484                    this.renderSlides();
485                    this.updateTrack();
486                    this.updateNavVisibility();
487                    this.updateNavButtons();
488                }, 250);
489            });
490        }
491        
492        /**
493         * Autoplay starten
494         */
495        startAutoplay() {
496            this.autoplayTimer = setInterval(() => {
497                const maxIndex = this.slides.length - this.getSlidePerView();
498                
499                if (this.currentIndex < maxIndex) {
500                    this.next();
501                } else {
502                    this.currentIndex = 0;
503                    this.updateTrack();
504                }
505            }, this.config.autoplayInterval);
506        }
507        
508        /**
509         * Autoplay stoppen
510         */
511        stopAutoplay() {
512            if (this.autoplayTimer) {
513                clearInterval(this.autoplayTimer);
514                this.autoplayTimer = null;
515            }
516        }
517        
518        /**
519         * Destruktor
520         */
521        destroy() {
522            this.stopAutoplay();
523            this.prevBtn = null;
524            this.nextBtn = null;
525            this.track = null;
526            this.slides = [];
527        }
528    }
529    
530    /**
531     * Public API
532     */
533    return {
534        /**
535         * Slider initialisieren
536         */
537        init: function(containerOrSelector, config = {}) {
538            const instanceKey = containerOrSelector instanceof Element
539                ? containerOrSelector
540                : document.querySelector(containerOrSelector);
541
542            if (!instanceKey) {
543                return null;
544            }
545
546            // Mehrfach Initialisierung verhindern
547            if (instances.has(instanceKey)) {
548                return instances.get(instanceKey);
549            }
550            
551            const slider = new Slider(containerOrSelector, config);
552
553            if (!slider || !slider.container) {
554                return null;
555            }
556
557            instances.set(instanceKey, slider);
558            
559            return slider;
560        },
561        
562        /**
563         * Alle Slider initialisieren
564         */
565        initAll: function(config = {}) {
566            const containers = document.querySelectorAll('.content-slides');
567            const sliders = [];
568            
569            containers.forEach(container => {
570                const slider = this.init(container, config);
571                if (slider) {
572                    sliders.push(slider);
573                }
574            });
575            
576            return sliders;
577        },
578        
579        /**
580         * Instance abrufen
581         */
582        getInstance: function(containerSelector) {
583            const instanceKey = containerSelector instanceof Element
584                ? containerSelector
585                : document.querySelector(containerSelector);
586
587            return instanceKey ? instances.get(instanceKey) : undefined;
588        },
589        
590        /**
591         * Instance zerstören
592         */
593        destroy: function(containerSelector) {
594            const instanceKey = containerSelector instanceof Element
595                ? containerSelector
596                : document.querySelector(containerSelector);
597
598            if (instanceKey && instances.has(instanceKey)) {
599                instances.get(instanceKey).destroy();
600                instances.delete(instanceKey);
601            }
602        },
603        
604        /**
605         * Alle Instanzen zerstören
606         */
607        destroyAll: function() {
608            instances.forEach((slider) => {
609                slider.destroy();
610            });
611            instances.clear();
612        }
613    };
614})();
615
616/**
617 * Auf DOMContentLoaded alle Slider initialisieren
618 */
619document.addEventListener('DOMContentLoaded', function() {
620    window.ContentSlidesManager.initAll();
621});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.