https://www.buhl.de/wp-content/themes/wp-themes-buhl-cobe-theme/mo…/content-slides.js?ver=1790144012
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.