1 2const body=document.body; 3 4// --- Sticky Header 5(function(){ 6 const header = document.querySelector('.site-header'); 7 const spacer = document.getElementById('header-spacer'); 8 if (!header || !spacer) return; 9 10 const navRow = header.querySelector('.nav-row'); 11 const normalHeight = navRow ? navRow.offsetHeight : 180; 12 const stickyHeight = 60; 13 let isSticky = false; 14 15 function update() { 16 const scrollY = window.scrollY || document.documentElement.scrollTop; 17 const shouldBeSticky = scrollY > 150; 18 19 if (shouldBeSticky !== isSticky) { 20 isSticky = shouldBeSticky; 21 if (shouldBeSticky) { 22 header.classList.add('is-sticky'); 23 spacer.style.height = normalHeight + 'px'; 24 } else { 25 header.classList.remove('is-sticky'); 26 spacer.style.height = '0px'; 27 } 28 } 29 } 30 31 let raf = null; 32 window.addEventListener('scroll', function() { 33 if (!raf) { 34 raf = requestAnimationFrame(function() { 35 update(); 36 raf = null; 37 }); 38 } 39 }, {passive: true}); 40 41 update(); 42})(); 43 44// --- Position Mitmachen button between primary-nav and actions 45(function(){ 46 function init() { 47 const mitmachenBtn = document.querySelector('.nav-mitmachen'); 48 const primaryNav = document.querySelector('.primary-nav'); 49 const actions = document.querySelector('.header-actions'); 50 const navRow = document.querySelector('.nav-row'); 51 52 if (!mitmachenBtn || !primaryNav || !actions || !navRow) return; 53 54 function positionButton() { 55 if (window.innerWidth < 1161) { 56 mitmachenBtn.style.left = ''; 57 mitmachenBtn.style.right = ''; 58 return; 59 } 60 61 const navRowRect = navRow.getBoundingClientRect(); 62 const primaryNavRect = primaryNav.getBoundingClientRect(); 63 const actionsRect = actions.getBoundingClientRect(); 64 65 // Calculate the right edge of primary-nav and left edge of actions 66 const primaryNavRight = primaryNavRect.right - navRowRect.left; 67 const actionsLeft = actionsRect.left - navRowRect.left; 68 69 // Calculate midpoint 70 const midpoint = (primaryNavRight + actionsLeft) / 2; 71 72 // Position button at midpoint, centered on itself 73 const buttonWidth = mitmachenBtn.offsetWidth; 74 mitmachenBtn.style.left = (midpoint - buttonWidth / 2) + 'px'; 75 mitmachenBtn.style.right = ''; 76 } 77 78 // Position on load and resize 79 positionButton(); 80 mitmachenBtn.classList.add('nav-mitmachen--ready'); 81 window.addEventListener('resize', positionButton); 82 83 // Async script may execute before stylesheet/fonts are applied â the initial 84 // measurement is then wrong. Re-measure once everything has finished loading. 85 window.addEventListener('load', positionButton); 86 if (document.fonts && document.fonts.ready) { 87 document.fonts.ready.then(positionButton); 88 } 89 90 // Also recalculate when header becomes sticky/unsticky 91 const header = document.querySelector('.site-header'); 92 if (header) { 93 const observer = new MutationObserver(positionButton); 94 observer.observe(header, { attributes: true, attributeFilter: ['class'] }); 95 } 96 } 97 98 if (document.readyState === 'loading') { 99 document.addEventListener('DOMContentLoaded', init); 100 } else { 101 init(); 102 } 103})(); 104 105// --- Search overlay 106const searchOpen=document.getElementById('searchOpen'); 107const searchCloseBtn=document.getElementById('searchClose'); 108const searchOverlay=document.getElementById('searchOverlay'); 109function openSearch(){ if(!searchOverlay) return; searchOverlay.setAttribute('aria-hidden','false'); body.classList.add('no-scroll'); } 110function closeSearch(){ if(!searchOverlay) return; searchOverlay.setAttribute('aria-hidden','true'); body.classList.remove('no-scroll'); } 111searchOpen?.addEventListener('click',openSearch,{passive:true}); 112searchCloseBtn?.addEventListener('click',closeSearch,{passive:true}); 113searchOverlay?.addEventListener('click',e=>{ if(e.target.matches('[data-close="search"]')) closeSearch(); },{passive:true}); 114document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&searchOverlay?.getAttribute('aria-hidden')==='false') closeSearch(); }); 115 116// --- Basic sliders (multi-instance) 117(function(){ 118 document.querySelectorAll('.basic-slider').forEach(section => { 119 const scroller = section.querySelector('.basic-scroll'); 120 const track = section.querySelector('.basic-track'); 121 const left = section.querySelector('.basic-btn[data-basic-dir="left"]'); 122 const right = section.querySelector('.basic-btn[data-basic-dir="right"]'); 123 if(!scroller || !track || !left || !right) return; 124 125 let unit = 0; 126 let peek = 0; 127 128 const measure = ()=>{ 129 const firstCard = track.querySelector('.basic-card'); 130 if(!firstCard) return; 131 const style = window.getComputedStyle(track); 132 const gap = parseFloat(style.columnGap || style.gap || '0'); 133 unit = firstCard.getBoundingClientRect().width + gap; 134 peek = Math.max(24, Math.round(unit * 0.25)); // keep a small preview visible 135 }; 136 137 const update = ()=>{ 138 const max = scroller.scrollWidth - scroller.clientWidth; 139 left.disabled = scroller.scrollLeft <= 0; 140 right.disabled = scroller.scrollLeft >= max - 1; 141 }; 142 143 const nudge = dir => { 144 const max = scroller.scrollWidth - scroller.clientWidth; 145 if(!unit){ measure(); } 146 147 // slide by nearly a viewport, leaving a small peek of the next/prev card 148 const stride = Math.max(unit, scroller.clientWidth - peek); 149 const current = scroller.scrollLeft; 150 let target = dir==='right' 151 ? Math.min(max, current + stride) 152 : Math.max(0, current - stride); 153 154 // snap to card boundary while keeping the initial "cut on right" behavior 155 if(target !== max && unit){ 156 target = Math.max(0, Math.floor(target / unit) * unit); 157 } 158 159 scroller.scrollTo({ left: target, behavior: 'smooth' }); 160 }; 161 162 left.addEventListener('click', ()=> nudge('left'), {passive:true}); 163 right.addEventListener('click', ()=> nudge('right'), {passive:true}); 164 scroller.addEventListener('scroll', update, {passive:true}); 165 window.addEventListener('resize', ()=>{ measure(); update(); }, {passive:true});
166 // Karten nachträglich eingefügt (z. B. news-slider.js) â neu vermessen 167 section.addEventListener('basic-slider:refresh', ()=>{ measure(); update(); }, {passive:true}); 168 169 measure(); 170 update(); 171 }); 172})(); 173// 174 175// --- Drawer (mobile) 176(function(){ 177 const drawer=document.getElementById('mobileDrawer'); 178 const drawerToggle=document.getElementById('drawerToggle'); 179 const drawerCloseFloating=document.getElementById('drawerCloseFloating'); 180 181 function openDrawer(){ 182 if(!drawer) return; 183 drawer.setAttribute('aria-hidden','false'); 184 drawerToggle?.setAttribute('aria-expanded','true'); 185 body.classList.add('no-scroll'); 186 if(window.matchMedia('(max-width:1160px)').matches){ 187 setTimeout(function(){ 188 drawerCloseFloating?.classList.add('is-visible'); 189 }, 240); 190 } 191 } 192 function closeDrawer(){ 193 if(!drawer) return; 194 drawer.setAttribute('aria-hidden','true'); 195 drawerToggle?.setAttribute('aria-expanded','false'); 196 body.classList.remove('no-scroll'); 197 drawerCloseFloating?.classList.remove('is-visible'); 198 drawerToggle?.focus(); 199 } 200 const openHandler = ()=>{ const isOpen = drawer?.getAttribute('aria-hidden')==='false'; isOpen ? closeDrawer() : openDrawer(); }; 201 drawerToggle?.addEventListener('click', openHandler, {passive:true}); 202 drawerToggle?.addEventListener('touchstart', e=>{ e.preventDefault(); openHandler(); }, {passive:false}); 203 drawerCloseFloating?.addEventListener('click', closeDrawer, {passive:true}); 204 drawer?.addEventListener('click',e=>{ if(e.target.matches('[data-close="drawer"]')) closeDrawer(); },{passive:true}); 205 document.addEventListener('keydown',e=>{ if(e.key==='Escape'&&drawer?.getAttribute('aria-hidden')==='false') closeDrawer(); }); 206 207 // Auto-close drawer when resizing to desktop 208 const mqDesktop = window.matchMedia('(min-width: 1161px)'); 209 mqDesktop.addEventListener('change', (ev)=>{ if(ev.matches && drawer?.getAttribute('aria-hidden')==='false') closeDrawer(); }); 210 211 // Drawer menu animations 212 document.querySelectorAll('details.m-acc').forEach(details => { 213 let isAnimating = false; 214 215 details.addEventListener('click', function(e) { 216 // If clicking on a link, allow it to work normally (follow href) 217 if (e.target.closest('a')) { 218 return; 219 } 220 221 // Only handle clicks on summary element 222 if (!e.target.closest('summary')) { 223 return; 224 } 225 if (isAnimating) { 226 // e.preventDefault(); 227 // return; 228 } 229 230 if (this.open) { 231 // Closing - prevent default and animate out 232 e.preventDefault(); 233 isAnimating = true; 234 this.classList.add('closing'); 235 236 // Close after animation 237 setTimeout(() => { 238 this.open = false; 239 this.classList.remove('closing'); 240 isAnimating = false; 241 }, 250); 242 } else { 243 // Opening - allow default behavior 244 this.classList.remove('closing'); 245 } 246 }); 247 }); 248})(); 249 250// Newsletter card click handlers and sync with form checkboxes 251(function() { 252 // Find the container (nl-grid or form) for an element 253 function findContainer(element) { 254 return element.closest('.nl-grid') || element.closest('form') || element.closest('section.newsletter'); 255 } 256 257 // Find form checkbox by value within a specific container 258 function findFormCheckboxByValue(value, container) { 259 if (!container) return null; 260 return container.querySelector(`input[type="checkbox"][name*="multicheckbox"][value="${value}"]`); 261 } 262 263 // Find nl-card by form checkbox value within a specific container 264 function findCardByFormValue(formValue, container) { 265 if (!container) return null; 266 return container.querySelector(`.nl-card[data-form-value="${formValue}"]`); 267 } 268 269 // Sync nl-card checkbox with form checkbox 270 function syncCardToForm(card) { 271 const formValue = card.getAttribute('data-form-value'); 272 if (!formValue) return; 273 274 const container = findContainer(card); 275 const formCheckbox = findFormCheckboxByValue(formValue, container); 276 const cardCheckbox = card.querySelector('input[type="checkbox"]'); 277 278 if (formCheckbox && cardCheckbox) { 279 formCheckbox.checked = cardCheckbox.checked; 280 } 281 } 282 283 // Sync form checkbox with nl-card checkbox 284 function syncFormToCard(formCheckbox) { 285 const formValue = formCheckbox.value; 286 if (!formValue) return; 287 288 const container = findContainer(formCheckbox); 289 const card = findCardByFormValue(formValue, container); 290 const cardCheckbox = card?.querySelector('input[type="checkbox"]'); 291 292 if (cardCheckbox) { 293 cardCheckbox.checked = formCheckbox.checked; 294 } 295 } 296 297 // Set up nl-card checkboxes 298 document.querySelectorAll('.nl-card[data-checkbox]').forEach(card => { 299 const checkboxId = card.getAttribute('data-checkbox'); 300 // Find checkbox within the same container as the card to avoid conflicts 301 const container = findContainer(card); 302 const checkbox = container ? container.querySelector(`#${checkboxId}`) : document.getElementById(checkboxId); 303 const label = card.querySelector(`label[for="${checkboxId}"]`); 304 305 if (checkbox) { 306 // Card click handler - toggle when clicking anywhere on the card (except checkbox/label) 307 card.addEventListener('click', (e) => { 308 // If clicking directly on checkbox or label, let native behavior handle it 309 // The change event will trigger syncCardToForm 310 if (e.target === checkbox || e.target === label || e.target.closest('label') === label) { 311 return; 312 } 313 // Clicking elsewhere on card - manually toggle 314 checkbox.checked = !checkbox.checked; 315 syncCardToForm(card); 316 }); 317 318 // Checkbox change handler - sync whenever checkbox state changes 319 // This fires for: direct checkbox clicks, label clicks, and card clicks 320 checkbox.addEventListener('change', function() { 321 syncCardToForm(card); 322 }); 323 } 324 }); 325 326 // Set up form checkboxes 327 document.querySelectorAll('input[type="checkbox"][name*="multicheckbox"]').forEach(formCheckbox => { 328 formCheckbox.addEventListener('change', function() { 329 syncFormToCard(this); 330 }); 331 }); 332})(); 333 334// Newsletter: Validierung bei Klick auf .nl-btn (E-Mail + mind. eine Checkbox) 335function isValidEmail(value) { 336 if (!value || typeof value !== 'string') return false;
337 const trimmed = value.trim(); 338 if (!trimmed) return false; 339 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 340 return emailRegex.test(trimmed); 341} 342 343document.addEventListener('submit', function(e) { 344 const form = e.target; 345 if (!form || !form.querySelector || !form.querySelector('.nl-btn')) return; 346 const emailInput = form.querySelector('.nl-input[type="email"]') || form.querySelector('input[name*="email"]'); 347 if (emailInput) { 348 const email = (emailInput.value || '').trim(); 349 if (!email) { 350 e.preventDefault(); 351 alert('Bitte geben Sie Ihre E-Mail-Adresse ein.'); 352 emailInput.focus(); 353 return; 354 } 355 if (!isValidEmail(email) || (emailInput.checkValidity && !emailInput.checkValidity())) { 356 e.preventDefault(); 357 alert('Bitte geben Sie eine gültige E-Mail-Adresse ein.'); 358 emailInput.focus(); 359 return; 360 } 361 } 362 const checkboxContainer = form.closest('.nl-grid') || form.closest('.newsletter') || form; 363 const selectableCheckboxes = checkboxContainer.querySelectorAll('.nl-card input[type="checkbox"]'); 364 if (selectableCheckboxes.length > 0) { 365 const checked = checkboxContainer.querySelectorAll('.nl-card input[type="checkbox"]:checked'); 366 if (checked.length === 0) { 367 e.preventDefault(); 368 alert('Bitte wählen Sie mindestens einen Newsletter aus.'); 369 return; 370 } 371 } 372}, true); 373 374// Newsletter form submission 375const newsletterForm = document.getElementById('newsletterForm'); 376if (newsletterForm) { 377 newsletterForm.addEventListener('submit', function(e) { 378 e.preventDefault(); 379 const email = this.querySelector('.nl-input[type="email"]').value; 380 const anrede = this.querySelector('select[name="anrede"]') ? this.querySelector('select[name="anrede"]').value : ''; 381 const titel = this.querySelector('select[name="titel"]') ? this.querySelector('select[name="titel"]').value : ''; 382 const vorname = this.querySelector('input[name="vorname"]') ? this.querySelector('input[name="vorname"]').value : ''; 383 const nachname = this.querySelector('input[name="nachname"]') ? this.querySelector('input[name="nachname"]').value : ''; 384 const container = this.closest('.nl-grid') || this.closest('.newsletter') || document; 385 const selectableCheckboxes = container.querySelectorAll('.nl-card input[type="checkbox"]'); 386 const checked = Array.from(container.querySelectorAll('.nl-card input[type="checkbox"]:checked')).map(cb => cb.id); 387 if (selectableCheckboxes.length > 0 && checked.length === 0) { 388 alert('Bitte wählen Sie mindestens einen Newsletter aus.'); 389 return; 390 } 391 let alertText = 'E-Mail: ' + email + '\n\n'; 392 if (anrede) alertText += 'Anrede: ' + anrede + '\n'; 393 if (titel) alertText += 'Titel: ' + titel + '\n'; 394 if (vorname) alertText += 'Vorname: ' + vorname + '\n'; 395 if (nachname) alertText += 'Nachname: ' + nachname + '\n'; 396 alertText += '\nNewsletter IDs: ' + checked.join(', '); 397 alert(alertText); 398 }); 399} 400 401// Newsletter form submission for content form
402const newsletterFormContent = document.getElementById('newsletterFormContent'); 403if (newsletterFormContent) { 404 newsletterFormContent.addEventListener('submit', function(e) { 405 e.preventDefault(); 406 const email = this.querySelector('.nl-input[type="email"]').value; 407 const anrede = this.querySelector('select[name="anrede"]') ? this.querySelector('select[name="anrede"]').value : ''; 408 const titel = this.querySelector('select[name="titel"]') ? this.querySelector('select[name="titel"]').value : ''; 409 const vorname = this.querySelector('input[name="vorname"]') ? this.querySelector('input[name="vorname"]').value : ''; 410 const nachname = this.querySelector('input[name="nachname"]') ? this.querySelector('input[name="nachname"]').value : ''; 411 const container = this.closest('.nl-grid') || this.closest('.newsletter') || document; 412 const selectableCheckboxes = container.querySelectorAll('.nl-card input[type="checkbox"]'); 413 const checked = Array.from(container.querySelectorAll('.nl-card input[type="checkbox"]:checked')).map(cb => cb.id); 414 if (selectableCheckboxes.length > 0 && checked.length === 0) { 415 alert('Bitte wählen Sie mindestens einen Newsletter aus.'); 416 return; 417 } 418 let alertText = 'E-Mail: ' + email + '\n\n'; 419 if (anrede) alertText += 'Anrede: ' + anrede + '\n'; 420 if (titel) alertText += 'Titel: ' + titel + '\n'; 421 if (vorname) alertText += 'Vorname: ' + vorname + '\n'; 422 if (nachname) alertText += 'Nachname: ' + nachname + '\n'; 423 alertText += '\nNewsletter IDs: ' + checked.join(', '); 424 alert(alertText); 425 }); 426} 427 428// Show additional fields when email input is focused 429const nlEmailInputs = document.querySelectorAll('.nl-input[type="email"]'); 430nlEmailInputs.forEach(function(emailInput) { 431 emailInput.addEventListener('focus', function() { 432 const form = this.closest('.nl-form'); 433 if (form) { 434 const additionalFields = form.querySelector('.nl-additional-fields'); 435 const closeLink = form.querySelector('.nl-close-link'); 436 if (additionalFields) { 437 additionalFields.classList.remove('nl-additional-fields--hidden'); 438 } 439 if (closeLink) { 440 closeLink.classList.remove('nl-close-link--hidden'); 441 } 442 } 443 }); 444}); 445 446// Hide additional fields when close link is clicked 447const nlCloseLinks = document.querySelectorAll('.nl-close-link'); 448nlCloseLinks.forEach(function(closeLink) { 449 closeLink.addEventListener('click', function(e) { 450 e.preventDefault(); 451 const form = this.closest('.nl-form'); 452 if (form) { 453 const additionalFields = form.querySelector('.nl-additional-fields'); 454 if (additionalFields) { 455 additionalFields.classList.add('nl-additional-fields--hidden'); 456 } 457 this.classList.add('nl-close-link--hidden'); 458 } 459 }); 460}); 461 462// Handle select color based on selected value (first option = gray, others = black)
463const nlSelects = document.querySelectorAll('select.nl-input'); 464nlSelects.forEach(function(select) { 465 function updateSelectColor() { 466 if (select.value === '') { 467 select.classList.add('nl-input--default'); 468 } else { 469 select.classList.remove('nl-input--default'); 470 } 471 } 472 updateSelectColor(); // Set initial state 473 select.addEventListener('change', updateSelectColor); 474}); 475 476// Generic "load more" â .ev-more buttons; config from data-* or section context (no HTML required) 477function getLoadMoreConfig(button) { 478 if (button.dataset.loadMoreHiddenClass) { 479 return { 480 hiddenClass: button.dataset.loadMoreHiddenClass, 481 step: Number.parseInt(button.dataset.loadMoreStep || '0', 10), 482 scopeSelector: button.dataset.loadMoreScope || null, 483 hideMode: button.dataset.loadMoreHide || 'self' 484 }; 485 } 486 if (button.closest('.person-profil-articles-news')) { 487 return { 488 hiddenClass: 'person-profil-articles-news__card--hidden', 489 step: 5, 490 scopeSelector: '.person-profil-articles-news', 491 hideMode: 'self' 492 }; 493 } 494 if (button.closest('.person-profil-subsection--jesuiten')) { 495 return { 496 hiddenClass: 'person-profil-jesuiten-card--hidden', 497 step: 3, 498 scopeSelector: '.person-profil-subsection--jesuiten', 499 hideMode: 'self' 500 }; 501 } 502 return { 503 hiddenClass: 'ev-item--hidden', 504 step: 6, 505 scopeSelector: '.ev', 506 hideMode: 'self' 507 }; 508} 509 510document.querySelectorAll('.ev-more').forEach(function(button) { 511 const config = getLoadMoreConfig(button); 512 const hiddenClass = config.hiddenClass; 513 const step = config.step; 514 const hideMode = config.hideMode; 515 const scope = config.scopeSelector 516 ? (button.closest(config.scopeSelector) || document) 517 : (button.closest('section') || document); 518 519 const getHiddenItems = function() { 520 return scope.querySelectorAll('.' + hiddenClass); 521 }; 522 523 const getHideTarget = function() { 524 if (hideMode.startsWith('closest:')) { 525 const selector = hideMode.slice('closest:'.length).trim(); 526 return selector ? (button.closest(selector) || button) : button; 527 } 528 return button; 529 }; 530 531 const hideTarget = getHideTarget(); 532 const updateVisibility = function() { 533 const hasHiddenItems = getHiddenItems().length > 0; 534 hideTarget.style.display = hasHiddenItems ? '' : 'none'; 535 }; 536 537 updateVisibility(); 538 539 button.addEventListener('click', function() { 540 const hiddenItems = getHiddenItems(); 541 const itemsToShow = step > 0 ? Math.min(step, hiddenItems.length) : hiddenItems.length; 542 543 for (let i = 0; i < itemsToShow; i++) { 544 hiddenItems[i].classList.remove(hiddenClass); 545 } 546 547 updateVisibility(); 548 }); 549}); 550 551// ===== Big Slider ===== 552(function(){ 553 const slider = document.querySelector('.big-slider'); 554 if (!slider) return; 555 556 const scroll = slider.querySelector('.big-slider__scroll'); 557 const cards = Array.from(slider.querySelectorAll('.big-slider__card')); 558 const dots = Array.from(slider.querySelectorAll('.big-dot')); 559 const leftBtn = slider.querySelector('.big-slider-btn[data-big-dir="left"]'); 560 const rightBtn = slider.querySelector('.big-slider-btn[data-big-dir="right"]'); 561 562 let currentIndex = 0; 563 let isProgrammaticScroll = false; 564 565 const updateDots = () => { 566 dots.forEach((dot, index) => { 567 dot.classList.toggle('is-active', index === currentIndex); 568 }); 569 }; 570 571 // Calculate button width based on visible slide area 572 const updateButtonWidths = () => { 573 if (!leftBtn || !rightBtn || !scroll || !cards.length) return; 574 575 const isDesktop = window.matchMedia('(min-width: 900px)').matches; 576 if (!isDesktop) { 577 leftBtn.style.width = ''; 578 rightBtn.style.width = ''; 579 return; 580 } 581 582 const track = slider.querySelector('.big-slider__track'); 583 if (!track) return; 584 585 const scrollWidth = scroll.clientWidth; 586 const cardWidth = cards[0].getBoundingClientRect().width; 587 const trackStyles = getComputedStyle(track); 588 const gapValue = trackStyles.columnGap || trackStyles.gap || '0'; 589 const gap = parseFloat(gapValue) || 0; 590 591 // Formula: (scrollWidth - cardWidth - (2 * gap)) / 2 592 const buttonWidth = (scrollWidth - cardWidth - (2 * gap)) / 2; 593 594 if (buttonWidth > 0) { 595 leftBtn.style.width = buttonWidth + 'px'; 596 rightBtn.style.width = buttonWidth + 'px'; 597 } else { 598 leftBtn.style.width = ''; 599 rightBtn.style.width = ''; 600 } 601 }; 602 603 // Simple visibility control for desktop arrows 604 const updateButtons = () => { 605 if (!leftBtn || !rightBtn) return; 606 607 const isDesktop = window.matchMedia('(min-width: 900px)').matches; 608 609 if (!isDesktop) { 610 // Hide arrows completely on mobile/tablet 611 leftBtn.style.display = 'none'; 612 rightBtn.style.display = 'none'; 613 return; 614 } 615 616 // Update button widths 617 updateButtonWidths(); 618 619 // On desktop: show only when a previous/next slide exists 620 leftBtn.style.display = currentIndex === 0 ? 'none' : 'flex'; 621 rightBtn.style.display = currentIndex === cards.length - 1 ? 'none' : 'flex'; 622 }; 623 624 const scrollToCard = (index) => { 625 if (cards[index]) { 626 isProgrammaticScroll = true; 627 cards[index].scrollIntoView({ 628 behavior: 'smooth', 629 block: 'nearest', 630 inline: 'center' 631 }); 632 currentIndex = index; 633 updateDots(); 634 updateButtons(); 635 // Reset flag after scroll animation completes 636 setTimeout(() => { 637 isProgrammaticScroll = false;
638 }, 500); 639 } 640 }; 641 642 const nextCard = () => { 643 const next = (currentIndex + 1) % cards.length; 644 scrollToCard(next); 645 }; 646 647 const prevCard = () => { 648 const prev = currentIndex === 0 ? cards.length - 1 : currentIndex - 1; 649 scrollToCard(prev); 650 }; 651 652 653 // Dot navigation 654 dots.forEach((dot, index) => { 655 dot.addEventListener('click', () => { 656 scrollToCard(index); 657 }); 658 }); 659 660 // Card click navigation 661 cards.forEach((card, index) => { 662 card.addEventListener('click', () => { 663 if (index !== currentIndex) { 664 scrollToCard(index); 665 } 666 }); 667 }); 668 669 // Touch/swipe support 670 let touchStartX = 0; 671 let touchStartY = 0; 672 let touchEndX = 0; 673 let touchEndY = 0; 674 let isHorizontalSwipe = false; 675 676 scroll.addEventListener('touchstart', (e) => { 677 touchStartX = e.changedTouches[0].screenX; 678 touchStartY = e.changedTouches[0].screenY; 679 isHorizontalSwipe = false; 680 }, { passive: true }); 681 682 scroll.addEventListener('touchmove', (e) => { 683 if (!touchStartX || !touchStartY) return; 684 685 const touchCurrentX = e.changedTouches[0].screenX; 686 const touchCurrentY = e.changedTouches[0].screenY; 687 const moveDistanceX = Math.abs(touchCurrentX - touchStartX); 688 const moveDistanceY = Math.abs(touchCurrentY - touchStartY); 689 690 // If horizontal movement is greater than vertical, prevent scrolling 691 if (moveDistanceX > moveDistanceY && moveDistanceX > 10) { 692 isHorizontalSwipe = true; 693 e.preventDefault(); // Prevent scrolling during horizontal swipe 694 } 695 }, { passive: false }); 696 697 scroll.addEventListener('touchend', (e) => { 698 touchEndX = e.changedTouches[0].screenX; 699 touchEndY = e.changedTouches[0].screenY; 700 handleSwipe(); 701 }, { passive: true }); 702 703 const handleSwipe = () => { 704 const swipeDistanceX = Math.abs(touchEndX - touchStartX); 705 const swipeDistanceY = Math.abs(touchEndY - touchStartY); 706 707 // Only trigger if it's a horizontal swipe (horizontal movement > vertical movement) 708 if (swipeDistanceX <= swipeDistanceY || !isHorizontalSwipe) { 709 isHorizontalSwipe = false; 710 return; 711 } 712 713 // Mark that this was a swipe gesture - disable auto-snap 714 wasSwipeGesture = true; 715 716 // Directly jump to next/prev card without scrolling 717 if (touchEndX < touchStartX) { 718 // Swipe left - next card 719 nextCard(); 720 } else { 721 // Swipe right - prev card 722 prevCard(); 723 } 724 725 // Reset flags after scroll animation completes 726 setTimeout(() => { 727 wasSwipeGesture = false; 728 isHorizontalSwipe = false; 729 }, 600); 730 }; 731 732 // Button navigation (desktop controls) 733 if (leftBtn && rightBtn) { 734 leftBtn.addEventListener('click', () => { 735 prevCard(); 736 }); 737 rightBtn.addEventListener('click', () => { 738 nextCard(); 739 }); 740 } 741 742 // Initial state for dots and arrows 743 updateDots(); 744 updateButtons(); 745 746 // Update arrow visibility and widths on resize (e.g., when crossing desktop breakpoint) 747 window.addEventListener('resize', updateButtons); 748 749 // Handle scroll events to update dots using Intersection Observer 750 const observer = new IntersectionObserver((entries) => { 751 entries.forEach(entry => { 752 if (entry.isIntersecting) { 753 const cardIndex = cards.indexOf(entry.target); 754 if (cardIndex !== -1 && cardIndex !== currentIndex) { 755 currentIndex = cardIndex; 756 updateDots(); 757 updateButtons(); 758 } 759 } 760 }); 761 }, { 762 root: scroll, 763 threshold: 0.5 764 }); 765 766 // Observe all cards 767 cards.forEach(card => observer.observe(card)); 768 769 // Auto-snap to nearest card after manual scrolling 770 let scrollTimeout; 771 let isUserScrolling = false; 772 let wasSwipeGesture = false; 773 774 const findNearestCard = () => { 775 if (!scroll || cards.length === 0) return -1; 776 777 const scrollLeft = scroll.scrollLeft; 778 const scrollWidth = scroll.clientWidth; 779 const scrollCenter = scrollLeft + scrollWidth / 2; 780 781 let nearestIndex = 0; 782 let minDistance = Infinity; 783 784 cards.forEach((card, index) => { 785 // Get the card's position relative to the scroll container 786 const cardOffsetLeft = card.offsetLeft; 787 const cardWidth = card.offsetWidth; 788 const cardCenter = cardOffsetLeft + cardWidth / 2; 789 const distance = Math.abs(cardCenter - scrollCenter); 790 791 if (distance < minDistance) { 792 minDistance = distance; 793 nearestIndex = index; 794 } 795 }); 796 797 return nearestIndex; 798 }; 799 800 const handleScrollEnd = () => { 801 if (isProgrammaticScroll || !isUserScrolling || wasSwipeGesture) return; 802 803 isUserScrolling = false;
804 const nearestIndex = findNearestCard(); 805 806 // Always snap to nearest card, even if it's the same (to ensure perfect centering) 807 if (nearestIndex !== -1) { 808 scrollToCard(nearestIndex); 809 } 810 }; 811 812 scroll.addEventListener('scroll', () => { 813 if (isProgrammaticScroll) return; 814 815 isUserScrolling = true; 816 clearTimeout(scrollTimeout); 817 818 // Wait for scroll to end before snapping 819 scrollTimeout = setTimeout(() => { 820 handleScrollEnd(); 821 }, 200); 822 }, { passive: true }); 823})(); 824 825// ===== Hero Image Slider ===== 826(function(){ 827 // Initialize all hero image sliders 828 const imageSliders = [ 829 document.getElementById('heroImageSlider'), 830 document.getElementById('heroImageSliderOverlay') 831 ].filter(Boolean); 832 833 imageSliders.forEach((imageSlider) => { 834 const slides = Array.from(imageSlider.querySelectorAll('.hero-image-slide')); 835 const controlButton = imageSlider.querySelector('.hero-image-control'); 836 837 if (!controlButton) return; 838 839 let currentImageIndex = 0; 840 let imageSlideTimer = null; 841 let isPaused = false; 842 843 // Sequential image preloading function 844 const preloadImagesSequentially = () => { 845 const imagesToPreload = slides.slice(1).map(slide => { 846 const img = slide.querySelector('img[data-src]'); 847 if (!img) return null; 848 const isMobile = window.innerWidth <= 920; 849 if (isMobile && img.hasAttribute('data-mobile-src')) { 850 return img.getAttribute('data-mobile-src'); 851 } 852 return img.getAttribute('data-src'); 853 }).filter(Boolean); 854 855 let preloadIndex = 0; 856 857 const preloadNext = () => { 858 if (preloadIndex >= imagesToPreload.length) return; 859 860 const img = new Image(); 861 img.onload = () => { 862 // Find the corresponding slide and set src (desktop or mobile) 863 const slide = slides[preloadIndex + 1]; 864 const slideImg = slide.querySelector('img[data-src]'); 865 if (slideImg) { 866 // Store original sources for resize handling 867 if (!slideImg.dataset.originalDesktopSrc) { 868 slideImg.dataset.originalDesktopSrc = slideImg.getAttribute('data-src'); 869 } 870 if (!slideImg.dataset.originalMobileSrc && slideImg.hasAttribute('data-mobile-src')) { 871 slideImg.dataset.originalMobileSrc = slideImg.getAttribute('data-mobile-src'); 872 } 873 874 const isMobile = window.innerWidth <= 920; 875 const srcAttr = (isMobile && slideImg.hasAttribute('data-mobile-src')) ? 'data-mobile-src' : 'data-src'; 876 slideImg.src = slideImg.getAttribute(srcAttr); 877 slideImg.removeAttribute('data-src'); 878 } 879 preloadIndex++; 880 // Preload next image after a short delay 881 setTimeout(preloadNext, 200); 882 }; 883 img.onerror = () => { 884 // Continue even if one image fails 885 preloadIndex++; 886 setTimeout(preloadNext, 200); 887 }; 888 img.src = imagesToPreload[preloadIndex]; 889 }; 890 891 // Start preloading after first image is loaded 892 const firstImg = slides[0].querySelector('img'); 893 if (firstImg && firstImg.complete) { 894 preloadNext(); 895 } else if (firstImg) { 896 firstImg.addEventListener('load', preloadNext, { once: true }); 897 } else { 898 preloadNext(); 899 } 900 }; 901 902 // Function to ensure image is loaded before showing 903 const ensureImageLoaded = (img, callback) => { 904 if (!img) { 905 callback(); 906 return; 907 } 908 909 // If image has data-src, load it first (respecting mobile variant) 910 if (img.hasAttribute('data-src')) { 911 // Store original sources for resize handling 912 if (!img.dataset.originalDesktopSrc) { 913 img.dataset.originalDesktopSrc = img.getAttribute('data-src'); 914 } 915 if (!img.dataset.originalMobileSrc && img.hasAttribute('data-mobile-src')) { 916 img.dataset.originalMobileSrc = img.getAttribute('data-mobile-src'); 917 } 918 919 const isMobile = window.innerWidth <= 920; 920 const srcAttr = (isMobile && img.hasAttribute('data-mobile-src')) ? 'data-mobile-src' : 'data-src'; 921 img.src = img.getAttribute(srcAttr); 922 img.removeAttribute('data-src'); 923 } 924 925 // Check if image is already loaded 926 if (img.complete && img.naturalHeight !== 0) { 927 callback(); 928 return; 929 } 930 931 // Wait for image to load 932 img.onload = () => { 933 callback(); 934 }; 935 img.onerror = () => { 936 // Continue even if image fails to load 937 callback(); 938 }; 939 }; 940 941 // Ensure first slide image is loaded immediately (important for overlay hero with data-src only) 942 const firstSlide = slides[0]; 943 if (firstSlide) { 944 const firstImg = firstSlide.querySelector('img'); 945 if (firstImg) { 946 // Store original sources for resize handling (even if image already has src) 947 if (firstImg.hasAttribute('data-src') && !firstImg.dataset.originalDesktopSrc) { 948 firstImg.dataset.originalDesktopSrc = firstImg.getAttribute('data-src'); 949 } 950 if (firstImg.hasAttribute('data-mobile-src') && !firstImg.dataset.originalMobileSrc) { 951 firstImg.dataset.originalMobileSrc = firstImg.getAttribute('data-mobile-src'); 952 } 953 // If image already has src, also store it as desktop source 954 if (firstImg.src && !firstImg.dataset.originalDesktopSrc && !firstImg.hasAttribute('data-src')) { 955 firstImg.dataset.originalDesktopSrc = firstImg.src; 956 } 957 ensureImageLoaded(firstImg, () => {}); 958 } 959 } 960 961 // Function to start image slideshow 962 const startImageSlideshow = () => { 963 imageSlideTimer = setInterval(() => { 964 const nextIndex = (currentImageIndex + 1) % slides.length; 965 const nextSlide = slides[nextIndex]; 966 const nextImg = nextSlide.querySelector('img'); 967 968 // Ensure image is loaded before showing 969 ensureImageLoaded(nextImg, () => {
970 slides.forEach(slide => slide.classList.remove('is-active')); 971 currentImageIndex = nextIndex; 972 nextSlide.classList.add('is-active'); 973 }); 974 }, 4000); // Change image every 4 seconds 975 }; 976 977 // Function to stop image slideshow 978 const stopImageSlideshow = () => { 979 if (imageSlideTimer) { 980 clearInterval(imageSlideTimer); 981 imageSlideTimer = null; 982 } 983 }; 984 985 // Function to toggle pause/play 986 const toggleImageSlideshow = () => { 987 if (isPaused) { 988 startImageSlideshow(); 989 isPaused = false; 990 updateControlButton(); 991 } else { 992 stopImageSlideshow(); 993 isPaused = true; 994 updateControlButton(); 995 } 996 }; 997 998 // Function to update control button icon and label 999 const updateControlButton = () => { 1000 if (isPaused) { 1001 // Show play icon (filled triangle) 1002 controlButton.innerHTML = ` 1003 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 1004 <polygon points="5,3 19,12 5,21"></polygon> 1005 </svg> 1006 `; 1007 controlButton.setAttribute('aria-label', 'Play slideshow'); 1008 } else { 1009 // Show pause icon (two filled rectangles) 1010 controlButton.innerHTML = ` 1011 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> 1012 <rect x="6" y="4" width="4" height="16"></rect> 1013 <rect x="14" y="4" width="4" height="16"></rect> 1014 </svg> 1015 `; 1016 controlButton.setAttribute('aria-label', 'Pause slideshow'); 1017 } 1018 }; 1019 1020 // Add event listener for control button 1021 controlButton.addEventListener('click', toggleImageSlideshow); 1022 1023 // Start slideshow if multiple images 1024 if (slides.length > 1) { 1025 // Start preloading images sequentially 1026 preloadImagesSequentially(); 1027 startImageSlideshow(); 1028 } else { 1029 // Hide control button if only one image 1030 controlButton.style.display = 'none'; 1031 } 1032 1033 // Cleanup on page unload 1034 window.addEventListener('beforeunload', stopImageSlideshow); 1035 1036 // Handle resize to switch between mobile/desktop images for overlay hero 1037 if (imageSlider.id === 'heroImageSliderOverlay') { 1038 let lastViewportWidth = window.innerWidth; 1039 let isCurrentlyMobile = lastViewportWidth <= 920; 1040 1041 // Store original sources for all images
1042 slides.forEach(slide => { 1043 const img = slide.querySelector('img'); 1044 if (img) { 1045 if (!img.dataset.originalDesktopSrc && img.hasAttribute('data-src')) { 1046 img.dataset.originalDesktopSrc = img.getAttribute('data-src'); 1047 } 1048 if (!img.dataset.originalMobileSrc && img.hasAttribute('data-mobile-src')) { 1049 img.dataset.originalMobileSrc = img.getAttribute('data-mobile-src'); 1050 } 1051 } 1052 }); 1053 1054 const reloadImagesForViewport = () => { 1055 const currentWidth = window.innerWidth; 1056 const nowMobile = currentWidth <= 920; 1057 1058 // Only reload if we've crossed the breakpoint 1059 if (nowMobile !== isCurrentlyMobile) { 1060 isCurrentlyMobile = nowMobile; 1061 1062 slides.forEach(slide => { 1063 const img = slide.querySelector('img'); 1064 if (!img) return; 1065 1066 // Get stored sources or fall back to attributes 1067 let desktopSrc = img.dataset.originalDesktopSrc; 1068 let mobileSrc = img.dataset.originalMobileSrc; 1069 1070 // If not stored yet, try to get from attributes 1071 if (!desktopSrc) { 1072 desktopSrc = img.getAttribute('data-src') || (img.src && !nowMobile ? img.src : null); 1073 } 1074 if (!mobileSrc) { 1075 mobileSrc = img.getAttribute('data-mobile-src'); 1076 } 1077 1078 // If still no desktop source and image has src, use current src as desktop 1079 if (!desktopSrc && img.src && !img.src.includes('mobile')) { 1080 desktopSrc = img.src; 1081 img.dataset.originalDesktopSrc = desktopSrc; 1082 } 1083 1084 if (nowMobile && mobileSrc) { 1085 // Switch to mobile image 1086 img.src = mobileSrc; 1087 } else if (!nowMobile && desktopSrc) { 1088 // Switch to desktop image 1089 img.src = desktopSrc; 1090 } 1091 }); 1092 } 1093 1094 lastViewportWidth = currentWidth; 1095 }; 1096 1097 window.addEventListener('resize', reloadImagesForViewport, { passive: true }); 1098 } 1099 }); 1100})(); 1101 1102// ===== Hero Slider Height Adjustment ===== 1103(function(){ 1104const heroSlider = document.getElementById('heroSlider'); 1105if (!heroSlider) return; 1106 1107const image = heroSlider.querySelector('.hero-slider__image'); 1108const content = heroSlider.querySelector('.hero-slider__content'); 1109 1110if (!image || !content) return; 1111 1112const adjustImageHeight = () => { 1113 const contentHeight = content.offsetHeight; 1114 const imageNaturalHeight = image.offsetWidth * (554 / 831); // Calculate natural height from aspect ratio 1115 1116 if (contentHeight > imageNaturalHeight) { 1117 // Content is taller than image's natural height - stretch image to match content 1118 image.style.height = contentHeight + 'px'; 1119 image.style.aspectRatio = 'unset'; // Override aspect ratio 1120 } else { 1121 // Image's natural height is fine - restore natural proportions 1122 image.style.height = ''; 1123 image.style.aspectRatio = '831 / 554'; 1124 } 1125}; 1126 1127// Make function globally accessible 1128window.adjustHeroSliderHeight = adjustImageHeight; 1129 1130// Adjust on load and resize 1131adjustImageHeight(); 1132window.addEventListener('resize', adjustImageHeight); 1133 1134// Adjust when content changes (e.g., slide transitions) 1135const observer = new MutationObserver(adjustImageHeight); 1136observer.observe(content, { childList: true, subtree: true }); 1137})(); 1138 1139// ===== Hero Slider ===== 1140(function(){ 1141 // Initialize all hero sliders (skip overlay variant as it's no longer a slider) 1142 const sliders = [ 1143 document.getElementById('heroSlider')
1144 ].filter(Boolean); 1145 1146 sliders.forEach((slider) => { 1147 const slides = Array.from(slider.querySelectorAll('.hero-slider__slide')); 1148 const allDots = Array.from(slider.querySelectorAll('.hero-dot')); 1149 let currentSlide = 0; 1150 let autoPlayTimer = null; 1151 1152 let measureSlidesHeight = null; // Will be set for overlay variant 1153 1154 const goToSlide = (index) => { 1155 slides.forEach((slide, i) => { 1156 slide.classList.toggle('is-active', i === index); 1157 }); 1158 // Update all dots across all slides 1159 allDots.forEach((dot) => { 1160 const dotIndex = parseInt(dot.getAttribute('data-slide')); 1161 dot.classList.toggle('is-active', dotIndex === index); 1162 }); 1163 currentSlide = index; 1164 1165 // Trigger height adjustment after slide change 1166 setTimeout(() => { 1167 if (window.adjustHeroSliderHeight) { 1168 window.adjustHeroSliderHeight(); 1169 } 1170 // Also measure slides height for overlay variant 1171 if (measureSlidesHeight) { 1172 measureSlidesHeight(); 1173 } 1174 }, 100); // Small delay to ensure DOM has updated 1175 }; 1176 1177 const nextSlide = () => { 1178 const next = (currentSlide + 1) % slides.length; 1179 goToSlide(next); 1180 }; 1181 1182 const startAutoPlay = () => { 1183 stopAutoPlay(); 1184 // Auto-advance after 5 seconds 1185 autoPlayTimer = setTimeout(() => { 1186 nextSlide(); 1187 // Stop after reaching slide 2 1188 if (currentSlide === 1) { 1189 stopAutoPlay(); 1190 } 1191 }, 5000); 1192 }; 1193 1194 const stopAutoPlay = () => { 1195 if (autoPlayTimer) { 1196 clearTimeout(autoPlayTimer); 1197 autoPlayTimer = null; 1198 } 1199 }; 1200 1201 // Dot click handlers - delegate from slider 1202 slider.addEventListener('click', (e) => { 1203 const dot = e.target.closest('.hero-dot'); 1204 if (dot) { 1205 const index = parseInt(dot.getAttribute('data-slide')); 1206 stopAutoPlay(); 1207 goToSlide(index); 1208 } 1209 }); 1210 1211 // Touch/swipe support 1212 let touchStartX = 0; 1213 let touchEndX = 0; 1214 1215 slider.addEventListener('touchstart', (e) => { 1216 touchStartX = e.changedTouches[0].screenX; 1217 }, { passive: true }); 1218 1219 slider.addEventListener('touchend', (e) => { 1220 touchEndX = e.changedTouches[0].screenX; 1221 handleSwipe(); 1222 }, { passive: true }); 1223 1224 const handleSwipe = () => { 1225 if (touchEndX < touchStartX - 50) { 1226 // Swipe left 1227 stopAutoPlay(); 1228 nextSlide(); 1229 } 1230 if (touchEndX > touchStartX + 50) { 1231 // Swipe right 1232 stopAutoPlay(); 1233 goToSlide(Math.max(0, currentSlide - 1)); 1234 } 1235 }; 1236 1237 // Start auto-play 1238 startAutoPlay(); 1239 1240 // Fix height jumping for overlay variant 1241 if (slider.classList.contains('hero-slider--overlay')) { 1242 const slidesContainer = slider.querySelector('.hero-slider__slides'); 1243 if (slidesContainer && slides.length > 0) { 1244 measureSlidesHeight = () => { 1245 let maxHeight = 0; 1246 const isMobile = window.innerWidth <= 900; 1247 const containerPadding = isMobile ? 20 + 16 : 40 + 20; // top + bottom padding 1248 const containerWidth = isMobile ? slidesContainer.offsetWidth : slidesContainer.offsetWidth - 128; // minus left + right padding on desktop 1249
1250 slides.forEach(slide => { 1251 // Temporarily show slide to measure 1252 const originalDisplay = slide.style.display; 1253 const originalPosition = slide.style.position; 1254 const originalOpacity = slide.style.opacity; 1255 1256 slide.style.display = 'block'; 1257 slide.style.position = 'static'; 1258 slide.style.opacity = '1'; 1259 slide.style.width = containerWidth + 'px'; 1260 1261 const height = slide.scrollHeight; 1262 if (height > maxHeight) { 1263 maxHeight = height; 1264 } 1265 1266 // Restore 1267 slide.style.display = originalDisplay; 1268 slide.style.position = originalPosition; 1269 slide.style.opacity = originalOpacity; 1270 slide.style.width = ''; 1271 }); 1272 1273 if (maxHeight > 0) { 1274 slidesContainer.style.height = (maxHeight + containerPadding) + 'px'; 1275 } 1276 }; 1277 1278 // Measure and set height after a short delay to ensure DOM is ready 1279 setTimeout(measureSlidesHeight, 200); 1280 window.addEventListener('resize', measureSlidesHeight); 1281 } 1282 } 1283 }); 1284})(); 1285 1286// ===== Hero Video Play/Pause Toggle ===== 1287(function(){ 1288const heroImageContainer = document.querySelector('.hero-slider--overlay .hero-slider__image'); 1289const heroVideo = document.querySelector('.hero-slider--overlay .hero-video'); 1290 1291if (heroImageContainer && heroVideo) { 1292 heroImageContainer.addEventListener('click', function(e) { 1293 // Only toggle if not clicking on interactive elements (dots, links) 1294 const target = e.target; 1295 if (target.closest('.hero-dot') || target.closest('.hero-slider__cta')) { 1296 return; 1297 } 1298 // Toggle video play/pause 1299 if (heroVideo.paused) { 1300 heroVideo.play(); 1301 } else { 1302 heroVideo.pause(); 1303 } 1304 }, {passive: true}); 1305} 1306})(); 1307 1308// ===== Mobile Image Handler ===== 1309(function(){ 1310const handleMobileImages = () => { 1311 const images = document.querySelectorAll('img[data-mobile-src]'); 1312 const isMobile = window.innerWidth <= 920; 1313 1314 images.forEach(img => { 1315 // Hero overlay slider images are handled separately by the hero slider logic 1316 if (img.closest('.hero-slider--overlay')) { 1317 return; 1318 } 1319 const mobileSrc = img.getAttribute('data-mobile-src'); 1320 const currentOriginal = img.getAttribute('data-original-src'); 1321 1322 if (isMobile && mobileSrc) { 1323 // On mobile, use mobile image 1324 if (currentOriginal === null) { 1325 const desktopSrc = img.getAttribute('src') || img.getAttribute('data-src') || ''; 1326 img.setAttribute('data-original-src', desktopSrc); 1327 } 1328 img.src = mobileSrc; 1329 } else if (!isMobile && currentOriginal !== null) { 1330 // On desktop, restore original image 1331 const original = img.getAttribute('data-original-src'); 1332 if (original) { 1333 img.src = original; 1334 } 1335 } 1336 }); 1337}; 1338 1339// Handle on load and resize 1340handleMobileImages(); 1341window.addEventListener('resize', handleMobileImages); 1342})(); 1343 1344 1345 1346/** 1347 * Minimal podcast controller 1348 * Expects feed.php â [{ title, pubDate, duration, audio }] 1349 * (Same shape as used on podcast.html.) 1350 */ 1351(function(){ 1352 const listEl = document.getElementById('pod-episodes'); 1353 if(!listEl) return; 1354 1355 // Single shared audio element 1356 const audio = new Audio(); 1357 audio.preload = 'none'; 1358 1359 let activeRow = null; 1360 let activeSrc = null; 1361 let raf = null; 1362 let isResetting = false; 1363 let rows = []; 1364 1365 // Cache and responsive media query 1366 let allEpisodes = []; 1367 const mqMobile = window.matchMedia('(max-width: 980px)'); 1368 1369 // helpers 1370 const setPlayIcon = (svg)=>{ if(!svg) return; svg.innerHTML=''; const p=document.createElementNS('http://www.w3.org/2000/svg','polygon'); p.setAttribute('points','6 1 6 23 20 12 6 1'); svg.appendChild(p); }; 1371 const setPauseIcon = (svg)=>{ if(!svg) return; svg.innerHTML=''; const r1=document.createElementNS('http://www.w3.org/2000/svg','rect'); r1.setAttribute('x','6'); r1.setAttribute('y','1'); svg.appendChild(r1); const r2=document.createElementNS('http://www.w3.org/2000/svg','rect'); r2.setAttribute('x','14'); r2.setAttribute('y','1'); svg.appendChild(r2); }; 1372 const fmtMinutes = (dur) => { 1373 if(!dur) return 'â'; 1374 const parts = dur.split(':').map(Number); 1375 let totalMin = 0; 1376 if(parts.length === 3){ totalMin = parts[0]*60 + parts[1] + parts[2]/60; } 1377 else if(parts.length === 2){ totalMin = parts[0] + parts[1]/60; } 1378 else { totalMin = (+parts[0] || 0) / 60; } 1379 return `${Math.round(totalMin)} min`; 1380 }; 1381 const stopAnim = ()=>{ if(raf){ cancelAnimationFrame(raf); raf=null; } }; 1382 1383 /** Matomo on-premise: trackEvent when an episode actually starts / resumes playback */ 1384 const trackMatomoPodcastPlay = () => { 1385 if (typeof window._paq === 'undefined' || typeof window._paq.push !== 'function') return; 1386 if (!activeRow) return; 1387 const rowData = rows.find(function (r) { return r.row === activeRow; }); 1388 if (!rowData || !rowData.ep) return; 1389 const ep = rowData.ep; 1390 const title = ep.title || ''; 1391 window._paq.push(['trackEvent', 'Podcast', 'Episode Play', title]); 1392 }; 1393 1394 const render = (episodes) => { 1395 listEl.innerHTML = ''; 1396 listEl.setAttribute('aria-busy','false'); 1397 1398 const maxItems = mqMobile.matches ? 4 : 7; 1399 rows = [];
1400 episodes.slice(0, maxItems).forEach((ep, idx)=>{ 1401 const row = document.createElement('div'); 1402 row.className = 'pod-ep'; 1403 row.setAttribute('role','listitem'); 1404 row.tabIndex = 0; 1405 row.dataset.src = ep.audio; 1406 1407 const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 1408 icon.setAttribute('xmlns','http://www.w3.org/2000/svg'); 1409 icon.setAttribute('viewBox','0 0 24 24'); 1410 icon.setAttribute('fill','currentColor'); 1411 icon.classList.add('pod-ep-icon'); 1412 // default to play icon 1413 const playPath1 = document.createElementNS('http://www.w3.org/2000/svg','polygon'); 1414 playPath1.setAttribute('points','6 1 6 23 20 12 6 1'); 1415 icon.appendChild(playPath1); 1416 1417 const title = document.createElement('h4'); 1418 title.className = 'pod-ep-title'; 1419 title.textContent = ep.title; 1420 1421 const meta = document.createElement('div'); 1422 meta.className = 'pod-ep-meta'; 1423 meta.textContent = fmtMinutes(ep.duration); 1424 1425 const progress = document.createElement('div'); 1426 progress.className = 'pod-ep-progress'; 1427 const bar = document.createElement('div'); 1428 bar.className = 'pod-ep-bar'; 1429 progress.appendChild(bar); 1430 1431 const player = document.createElement('div'); 1432 player.className = 'pod-ep-player'; 1433 player.appendChild(icon.cloneNode(true)); 1434 player.appendChild(progress.cloneNode(true)); 1435 1436 row.appendChild(title); 1437 row.appendChild(icon); 1438 row.appendChild(meta); 1439 row.appendChild(player); 1440 listEl.appendChild(row); 1441 rows.push({ row, bar, icon, ep, player, meta }); 1442 1443 const toggle = ()=>{ 1444 // if clicking current â toggle play/pause 1445 if(activeRow === row){ 1446 if(audio.paused){ 1447 audio.play().catch(()=>{}); 1448 row.classList.add('pod-ep--playing','pod-ep--active'); 1449 const prog = row.querySelector('.pod-ep-progress'); if(prog){ prog.style.display=''; } 1450 } else { 1451 audio.pause(); 1452 } 1453 return; 1454 } 1455 // open a new episode 1456 if(activeRow){ 1457 activeRow.classList.remove('pod-ep--playing','pod-ep--active'); 1458 const b = activeRow.querySelector('.pod-ep-bar'); 1459 if(b) b.style.width = '0%'; 1460 const playerProg = activeRow.querySelector('.pod-ep-player .pod-ep-progress'); 1461 if(playerProg) playerProg.style.display = ''; 1462 // reset old icon to play 1463 const oldIcon = activeRow.querySelector('.pod-ep-icon'); 1464 if(oldIcon){ oldIcon.innerHTML=''; const p=document.createElementNS('http://www.w3.org/2000/svg','polygon'); p.setAttribute('points','8 5 19 12 8 19 8 5'); oldIcon.appendChild(p); } 1465 // Restore original duration display 1466 const metaEl = activeRow.querySelector('.pod-ep-meta'); 1467 const oldRowData = rows.find(r => r.row === activeRow); 1468 if(metaEl && oldRowData){ 1469 metaEl.textContent = fmtMinutes(oldRowData.ep.duration); 1470 } 1471 } 1472 activeRow = row; 1473 activeSrc = ep.audio; 1474 row.classList.add('pod-ep--playing','pod-ep--active'); 1475 audio.src = ep.audio; 1476 audio.play().catch(()=>{ /* autoplay might be blocked; user can tap again */ }); 1477 tick(); // start progress animation 1478 // set icon to pause 1479 setPauseIcon(icon); 1480 // ensure progress becomes visible immediately 1481 const playerProgress = row.querySelector('.pod-ep-player .pod-ep-progress'); 1482 if(playerProgress) playerProgress.style.display = ''; 1483 }; 1484 1485 row.addEventListener('click', (e) => { 1486 // Don't toggle if clicking on the progress bar 1487 if (e.target.closest('.pod-ep-progress')) { 1488 e.stopPropagation(); 1489 return; 1490 } 1491 toggle(); 1492 }, {passive:true}); 1493 1494 // Add timeline click/drag functionality 1495 const playerProgress = player.querySelector('.pod-ep-progress'); 1496 if (playerProgress) { 1497 playerProgress.addEventListener('click', (e) => { 1498 e.stopPropagation(); 1499 if (!activeRow || activeRow !== row) return; 1500 1501 const rect = playerProgress.getBoundingClientRect(); 1502 const clickX = e.clientX - rect.left; 1503 const percentage = clickX / rect.width; 1504 const newTime = percentage * audio.duration; 1505 1506 if (!isNaN(newTime) && isFinite(newTime)) { 1507 audio.currentTime = newTime; 1508 } 1509 }); 1510 1511 // Add drag functionality 1512 let isDragging = false;
1513 playerProgress.addEventListener('mousedown', (e) => { 1514 e.stopPropagation(); 1515 if (!activeRow || activeRow !== row) return; 1516 isDragging = true; 1517 1518 const rect = playerProgress.getBoundingClientRect(); 1519 const clickX = e.clientX - rect.left; 1520 const percentage = Math.max(0, Math.min(1, clickX / rect.width)); 1521 const newTime = percentage * audio.duration; 1522 1523 if (!isNaN(newTime) && isFinite(newTime)) { 1524 audio.currentTime = newTime; 1525 } 1526 }); 1527 1528 document.addEventListener('mousemove', (e) => { 1529 if (!isDragging || !activeRow || activeRow !== row) return; 1530 1531 const rect = playerProgress.getBoundingClientRect(); 1532 const clickX = e.clientX - rect.left; 1533 const percentage = Math.max(0, Math.min(1, clickX / rect.width)); 1534 const newTime = percentage * audio.duration; 1535 1536 if (!isNaN(newTime) && isFinite(newTime)) { 1537 audio.currentTime = newTime; 1538 } 1539 }); 1540 1541 document.addEventListener('mouseup', () => { 1542 isDragging = false; 1543 }); 1544 } 1545 1546 row.addEventListener('keydown', (e)=>{ if(e.key==='Enter' || e.key===' '){ e.preventDefault(); toggle(); }}); 1547 }); 1548 1549 // Restore active UI after re-render (e.g., on responsive change) 1550 if (activeSrc) { 1551 const match = rows.find(r => r.ep.audio === activeSrc); 1552 if (match) { 1553 activeRow = match.row; 1554 match.row.classList.add('pod-ep--active'); 1555 const isPlaying = !audio.paused && (audio.src && audio.src.includes(activeSrc)); 1556 if (isPlaying) { 1557 match.row.classList.add('pod-ep--playing'); 1558 const playerIcon = match.row.querySelector('.pod-ep-player .pod-ep-icon'); 1559 if (playerIcon) setPauseIcon(playerIcon); 1560 } else { 1561 const playerIcon = match.row.querySelector('.pod-ep-player .pod-ep-icon'); 1562 if (playerIcon) setPlayIcon(playerIcon); 1563 } 1564 const duration = audio.duration || 0; 1565 if (duration > 0) { 1566 match.bar.style.width = ((audio.currentTime / duration) * 100).toFixed(2) + '%'; 1567 } 1568 } else { 1569 // If active episode got sliced out on mobile, clear UI ref 1570 activeRow = null; 1571 } 1572 } 1573 }; 1574 1575 const formatTime = (seconds) => { 1576 if (!seconds || isNaN(seconds)) return '0:00'; 1577 const mins = Math.floor(seconds / 60); 1578 const secs = Math.floor(seconds % 60); 1579 return `${mins}:${secs.toString().padStart(2, '0')}`; 1580 }; 1581 1582 const tick = ()=>{ 1583 stopAnim(); 1584 const update = ()=>{ 1585 if(!activeRow || isResetting) return; 1586 const bar = activeRow.querySelector('.pod-ep-bar'); 1587 const metaDisplay = activeRow.querySelector('.pod-ep-meta'); 1588 if(bar && audio.duration > 0){ 1589 bar.style.width = ((audio.currentTime / audio.duration) * 100).toFixed(2) + '%'; 1590 } 1591 if(metaDisplay && audio.duration > 0){ 1592 metaDisplay.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`; 1593 } 1594 raf = requestAnimationFrame(update); 1595 }; 1596 raf = requestAnimationFrame(update); 1597 }; 1598 1599 // audio events 1600 audio.addEventListener('play', ()=>{ 1601 if(activeRow){ 1602 activeRow.classList.add('pod-ep--playing'); 1603 const playerIcon = activeRow.querySelector('.pod-ep-player .pod-ep-icon'); 1604 if(playerIcon){ playerIcon.innerHTML=''; const r1=document.createElementNS('http://www.w3.org/2000/svg','rect'); r1.setAttribute('x','6'); r1.setAttribute('y','1'); playerIcon.appendChild(r1); const r2=document.createElementNS('http://www.w3.org/2000/svg','rect'); r2.setAttribute('x','14'); r2.setAttribute('y','1'); playerIcon.appendChild(r2); } 1605 const playerProg = activeRow.querySelector('.pod-ep-player .pod-ep-progress'); 1606 if(playerProg){ playerProg.style.display = ''; } 1607 } 1608 trackMatomoPodcastPlay(); 1609 tick(); 1610 }); 1611 audio.addEventListener('pause', ()=> { 1612 stopAnim(); 1613 if(activeRow){ 1614 activeRow.classList.remove('pod-ep--playing'); 1615 activeRow.classList.add('pod-ep--active'); 1616 const ic=activeRow.querySelector('.pod-ep-player .pod-ep-icon'); 1617 setPlayIcon(ic); 1618 // Restore original duration display 1619 const metaEl = activeRow.querySelector('.pod-ep-meta'); 1620 const rowData = rows.find(r => r.row === activeRow); 1621 if(metaEl && rowData){ 1622 metaEl.textContent = fmtMinutes(rowData.ep.duration); 1623 } 1624 } 1625 }); 1626 audio.addEventListener('ended', ()=>{ 1627 if(activeRow){ 1628 isResetting = true; 1629 1630 const rowToReset = activeRow; 1631 activeRow = null; 1632 1633 stopAnim(); 1634 if(raf) { 1635 cancelAnimationFrame(raf); 1636 raf = null; 1637 } 1638 1639 rowToReset.classList.remove('pod-ep--playing','pod-ep--active'); 1640 1641 const b = rowToReset.querySelector('.pod-ep-bar'); 1642 if(b) b.style.width = '0%'; 1643 1644 const playerProg = rowToReset.querySelector('.pod-ep-player'); 1645 if(playerProg) playerProg.style.display = ''; 1646 1647 const originalIcon = rowToReset.querySelector('.pod-ep-icon'); 1648 if(originalIcon){ 1649 originalIcon.innerHTML=''; 1650 const p=document.createElementNS('http://www.w3.org/2000/svg','polygon'); 1651 p.setAttribute('points','6 1 6 23 20 12 6 1'); 1652 originalIcon.appendChild(p); 1653 originalIcon.style.opacity = ''; 1654 } 1655 1656 const metaEl = rowToReset.querySelector('.pod-ep-meta'); 1657 const rowData = rows.find(r => r.row === rowToReset); 1658 if(metaEl && rowData){ 1659 metaEl.textContent = fmtMinutes(rowData.ep.duration); 1660 } 1661 1662 isResetting = false;
1663 } 1664 }); 1665 1666 // fetch episodes from the same feed your podcast page uses 1667 // (podcast-feed.php lives in scripts/ at the site root, next to podcast-cache/) 1668 fetch('scripts/podcast-feed.php') 1669 .then(r => r.json()) 1670 .then(data => { 1671 allEpisodes = Array.isArray(data) ? data : []; 1672 render(allEpisodes); 1673 }) 1674 .catch(err => { 1675 console.error('Feed-Fehler', err); 1676 listEl.innerHTML = '<div class="feed-error">Fehler beim Laden des Feeds.</div>'; 1677 listEl.setAttribute('aria-busy','false'); 1678 }); 1679 1680 // Re-render on viewport change to update the number of visible items 1681 mqMobile.addEventListener('change', () => { 1682 if (allEpisodes && allEpisodes.length) { 1683 render(allEpisodes); 1684 } 1685 }); 1686})(); 1687 1688// ===== Video logic moved to video.js ===== 1689// videoOverlayController, the One Minute Homily Click-to-Play / slider IIFE, 1690// the Videos Click-to-Play (Basic Slider) IIFE, the YouTube IFrame API lazy 1691// loader, and the consent overlay all live in typo3temp/assets/compressed/video.js 1692// (loaded with `defer` from each page's <head>). 1693 1694// ===== Schwerpunkte Dot Navigation ===== 1695(function(){ 1696 const schwerpunkteDots = document.querySelectorAll('.features--schwerpunkte .dot'); 1697 const schwerpunkteSlider = document.querySelector('.features--schwerpunkte .features-slider'); 1698 const schwerpunkteTrack = document.querySelector('.features--schwerpunkte .features-track'); 1699 let currentSchwerpunkteSlide = 0; 1700 1701 const updateSchwerpunkteDots = () => { 1702 schwerpunkteDots.forEach((dot, index) => { 1703 dot.classList.toggle('is-active', index === currentSchwerpunkteSlide); 1704 dot.setAttribute('aria-selected', index === currentSchwerpunkteSlide); 1705 }); 1706 }; 1707 1708 const scrollToSchwerpunkteSlide = (slideIndex) => { 1709 if (!schwerpunkteTrack) return; 1710 1711 const cards = schwerpunkteTrack.querySelectorAll('.features-card'); 1712 if (cards[slideIndex]) { 1713 cards[slideIndex].scrollIntoView({ 1714 behavior: 'smooth', 1715 block: 'nearest', 1716 inline: 'center' 1717 }); 1718 currentSchwerpunkteSlide = slideIndex; 1719 updateSchwerpunkteDots(); 1720 } 1721 }; 1722 1723 // Dot click handlers 1724 schwerpunkteDots.forEach((dot, index) => { 1725 dot.addEventListener('click', () => { 1726 scrollToSchwerpunkteSlide(index); 1727 }); 1728 }); 1729 1730 // Touch/swipe support for schwerpunkte slider 1731 let schwerpunkteTouchStartX = 0; 1732 let schwerpunkteTouchEndX = 0; 1733 let schwerpunkteTouchStartY = 0; 1734 let schwerpunkteTouchEndY = 0; 1735 1736 if (schwerpunkteSlider) { 1737 schwerpunkteSlider.addEventListener('touchstart', (e) => { 1738 schwerpunkteTouchStartX = e.changedTouches[0].screenX; 1739 schwerpunkteTouchStartY = e.changedTouches[0].screenY; 1740 }, { passive: true }); 1741 1742 schwerpunkteSlider.addEventListener('touchend', (e) => { 1743 schwerpunkteTouchEndX = e.changedTouches[0].screenX; 1744 schwerpunkteTouchEndY = e.changedTouches[0].screenY; 1745 handleSchwerpunkteSwipe(); 1746 }, { passive: true }); 1747 } 1748 1749 const handleSchwerpunkteSwipe = () => { 1750 const deltaX = schwerpunkteTouchEndX - schwerpunkteTouchStartX; 1751 const deltaY = schwerpunkteTouchEndY - schwerpunkteTouchStartY; 1752 1753 // Only treat as swipe if movement is mostly horizontal and exceeds threshold. 1754 if (Math.abs(deltaX) < 50 || Math.abs(deltaX) <= Math.abs(deltaY)) { 1755 return; 1756 } 1757 1758 if (deltaX < -50) { 1759 // Swipe left - next slide 1760 const nextSlide = (currentSchwerpunkteSlide + 1) % schwerpunkteDots.length; 1761 scrollToSchwerpunkteSlide(nextSlide); 1762 } 1763 if (deltaX > 50) { 1764 // Swipe right - previous slide 1765 const prevSlide = currentSchwerpunkteSlide === 0 ? schwerpunkteDots.length - 1 : currentSchwerpunkteSlide - 1; 1766 scrollToSchwerpunkteSlide(prevSlide); 1767 } 1768 }; 1769 1770 // Intersection Observer for schwerpunkte slides 1771 const schwerpunkteObserver = new IntersectionObserver((entries) => { 1772 entries.forEach(entry => { 1773 if (entry.isIntersecting) { 1774 const cardIndex = Array.from(schwerpunkteTrack.querySelectorAll('.features-card')).indexOf(entry.target); 1775 if (cardIndex !== -1 && cardIndex !== currentSchwerpunkteSlide) { 1776 currentSchwerpunkteSlide = cardIndex; 1777 updateSchwerpunkteDots(); 1778 } 1779 } 1780 }); 1781 }, { 1782 root: schwerpunkteSlider, 1783 threshold: 0.5 1784 }); 1785 1786 // Observe all schwerpunkte cards 1787 if (schwerpunkteTrack) { 1788 schwerpunkteTrack.querySelectorAll('.features-card').forEach(card => { 1789 schwerpunkteObserver.observe(card); 1790 }); 1791 } 1792 1793 // Recompute active dot on resize for schwerpunkte 1794 const recomputeSchwerpunkteActive = () => { 1795 if (!schwerpunkteSlider || !schwerpunkteTrack) return; 1796 const cards = Array.from(schwerpunkteTrack.querySelectorAll('.features-card')); 1797 if (!cards.length) return; 1798 const sliderRect = schwerpunkteSlider.getBoundingClientRect(); 1799 const sliderCenter = sliderRect.left + sliderRect.width / 2;
1800 let closestIdx = 0; 1801 let closestDist = Infinity; 1802 cards.forEach((card, idx) => { 1803 const rect = card.getBoundingClientRect(); 1804 const cardCenter = rect.left + rect.width / 2; 1805 const dist = Math.abs(cardCenter - sliderCenter); 1806 if (dist < closestDist) { closestDist = dist; closestIdx = idx; } 1807 }); 1808 if (closestIdx !== currentSchwerpunkteSlide) { 1809 currentSchwerpunkteSlide = closestIdx; 1810 updateSchwerpunkteDots(); 1811 } 1812 }; 1813 window.addEventListener('resize', recomputeSchwerpunkteActive, { passive: true }); 1814})(); 1815 1816// ===== Donation amount sync ===== 1817 (function(){ 1818 const donationSection = document.querySelector('.donation'); 1819 if (!donationSection) return; 1820 const amountField = donationSection.querySelector('.donation__amount-field'); 1821 const rangeField = donationSection.querySelector('.donation__range'); 1822 const purposeSelect = donationSection.querySelector('#donationPurpose'); 1823 const formEl = donationSection.querySelector('.donation__form'); 1824 if (!amountField || !rangeField) return; 1825 1826 const clampAmount = (value) => { 1827 const min = parseInt(rangeField.min, 10); 1828 const max = parseInt(rangeField.max, 10); 1829 const parsed = parseInt(value, 10); 1830 if (Number.isNaN(parsed)) return min; 1831 return Math.min(max, Math.max(min, parsed)); 1832 }; 1833 1834 const syncFromRange = () => { 1835 amountField.value = clampAmount(rangeField.value); 1836 const percent = ((rangeField.value - rangeField.min) / (rangeField.max - rangeField.min)) * 100; 1837 rangeField.style.setProperty('--_percent', percent + '%'); 1838 }; 1839 1840 const syncFromInput = () => { 1841 const clamped = clampAmount(amountField.value); 1842 amountField.value = clamped; 1843 rangeField.value = clamped; 1844 }; 1845 1846 rangeField.addEventListener('input', syncFromRange); 1847 amountField.addEventListener('change', syncFromInput); 1848 amountField.addEventListener('blur', syncFromInput); 1849 amountField.addEventListener('input', () => { 1850 const numeric = amountField.value.replace(/[^\d]/g, ''); 1851 const limited = numeric.slice(0, 4); // hard-limit to 4 digits while typing 1852 amountField.value = limited; 1853 const value = clampAmount(limited || rangeField.min); 1854 rangeField.value = value; 1855 const percent = ((value - rangeField.min) / (rangeField.max - rangeField.min)) * 100; 1856 rangeField.style.setProperty('--_percent', percent + '%'); 1857 }); 1858 syncFromRange(); 1859 1860 // Spendenzwecke aktuell von Twingle laden â serverseitig über scripts/twingle-targets.php, 1861 // der Browser kontaktiert twingle.de dabei nicht. Bei Fehlern bleiben die Optionen aus dem HTML. 1862 if (purposeSelect) { 1863 fetch('scripts/twingle-targets.php') 1864 .then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))) 1865 .then(data => { 1866 const targets = data && Array.isArray(data.targets) ? data.targets : []; 1867 if (!targets.length) return; 1868 const previous = purposeSelect.value; 1869 const options = targets.map(t => new Option(t.label, t.label)); 1870 if (data.freeInput) options.push(new Option('Freie Eingabe', 'other')); 1871 purposeSelect.replaceChildren(...options); 1872 if (options.some(o => o.value === previous)) purposeSelect.value = previous; 1873 }) 1874 .catch(err => console.warn('Spendenzwecke: Fallback auf HTML-Optionen', err)); 1875 } 1876 1877 // Open Twingle widget on submit 1878 if (formEl) { 1879 formEl.addEventListener('submit', function(e){ 1880 e.preventDefault(); 1881 const amount = clampAmount(amountField.value || rangeField.value); 1882 const purpose = purposeSelect ? (purposeSelect.value || purposeSelect.options[purposeSelect.selectedIndex].text) : ''; 1883 // Eigene Spendenseite mit eingebettetem Twingle-Formular (/form); sie reicht die tw_*-Parameter ans iframe weiter. 1884 // Direkt zu Twingle: 'https://spenden.twingle.de/freunde-der-gesellschaft-jesu-e-v/jesuiten-im-dienst-fuer-gott-und-die-menschen/tw677e97f080c21/page' 1885 //const base = 'jetzt-spenden.html'; 1886 const base = 'https://spenden.twingle.de/freunde-der-gesellschaft-jesu-e-v/jesuiten-im-dienst-fuer-gott-und-die-menschen/tw677e97f080c21/page'; 1887 // "Freie Eingabe" nur vorwählen (tw_target), dann zeigt Twingle das leere Freitextfeld. 1888 // Mit tw_target_fix=other stünde sonst wörtlich "other" im Feld. 1889 const target = purpose === 'other' 1890 ? `tw_target=${encodeURIComponent('Freie Eingabe')}` 1891 : `tw_target_fix=${encodeURIComponent(purpose)}`; 1892 const url = `${base}?tw_amount=${encodeURIComponent(String(amount))}&${target}&tw_rhythm=one_time`; 1893 //window.location.href = url; 1894 window.open(url, '_blank', 'noopener'); 1895 }); 1896 } 1897 })(); 1898 1899 // ===== Publication Region Modal ===== 1900 (function () { 1901 const langBtn = document.getElementById('langDropdown'); 1902 const modal = document.getElementById('modalPublicationRegion'); 1903 if (!langBtn || !modal) return; 1904 1905 // Open modal on button click 1906 langBtn.addEventListener('click', function() { 1907 modal.classList.add('active'); 1908 modal.style.display = 'flex'; 1909 }); 1910 1911 // Close modal on X button 1912 const closeBtn = modal.querySelector('.publication-modal-close'); 1913 if (closeBtn) { 1914 closeBtn.addEventListener('click', function() { 1915 modal.classList.remove('active'); 1916 modal.style.display = 'flex'; 1917 }); 1918 } 1919 1920 // Close modal when clicking outside 1921 modal.addEventListener('click', function(e) { 1922 if (e.target === modal) { 1923 modal.classList.remove('active'); 1924 modal.style.display = 'none'; 1925 } 1926 }); 1927 1928 // Handle region selection 1929 const regionLinks = modal.querySelectorAll('.region-link'); 1930 regionLinks.forEach(function(link) { 1931 link.addEventListener('click', function(e) { 1932 e.preventDefault(); 1933 1934 // Update button flag class using data attributes 1935 const btnFlag = langBtn.querySelector('.flag'); 1936 if (btnFlag) { 1937 btnFlag.classList.remove('publication-flag-de', 'publication-flag-at', 'publication-flag-ch'); 1938 if (link.dataset.flag.includes('Germany')) { 1939 btnFlag.classList.add('publication-flag-de'); 1940 } else if (link.dataset.flag.includes('Austria')) { 1941 btnFlag.classList.add('publication-flag-at'); 1942 } else if (link.dataset.flag.includes('Switzerland')) { 1943 btnFlag.classList.add('publication-flag-ch'); 1944 } 1945 } 1946 const labelSpan = langBtn.querySelector('.magazin__lang-label'); 1947 if (labelSpan && link.dataset.text) { 1948 labelSpan.textContent = link.dataset.text; 1949 } 1950 if (link.dataset.text) { 1951 langBtn.setAttribute('aria-label', link.dataset.text); 1952 } 1953 1954 // Hide modal 1955 modal.classList.remove('active'); 1956 }); 1957 }); 1958 })(); 1959 1960 // ===== Expandable copy ===== 1961 (function () { 1962 const copyWrapper = document.querySelector('.magazin__copy-wrapper'); 1963 const moreLink = copyWrapper ? copyWrapper.querySelector('.magazin__more-link') : null; 1964 1965 if (!copyWrapper || !moreLink) return; 1966 1967 moreLink.addEventListener('click', function(e) { 1968 e.preventDefault(); 1969 copyWrapper.classList.toggle('expanded'); 1970 if (copyWrapper.classList.contains('expanded')) { 1971 moreLink.textContent = 'Weniger'; 1972 } else { 1973 moreLink.textContent = 'Mehr'; 1974 } 1975 }); 1976 })(); 1977 1978// --- Donation Image Slideshow 1979(function(){ 1980 document.querySelectorAll('.donation__media[data-images]').forEach(function(container) { 1981 const imagesAttr = container.getAttribute('data-images'); 1982 if (!imagesAttr) return; 1983 1984 const imagePaths = imagesAttr.split(',').map(function(path) { 1985 return path.trim(); 1986 }).filter(function(path) { 1987 return path.length > 0; 1988 }); 1989 1990 // If only one or no images, keep it static 1991 if (imagePaths.length <= 1) { 1992 if (imagePaths.length === 1) { 1993 const existingImg = container.querySelector('img'); 1994 if (existingImg) { 1995 existingImg.src = imagePaths[0]; 1996 } else { 1997 const img = document.createElement('img'); 1998 img.src = imagePaths[0]; 1999 img.alt = 'Donation image'; 2000 container.appendChild(img); 2001 } 2002 } 2003 return; 2004 } 2005 2006 const existingImg = container.querySelector('img'); 2007 const altText = existingImg ? existingImg.getAttribute('alt') || '' : ''; 2008 2009 // Remove existing image 2010 if (existingImg) { 2011 existingImg.remove(); 2012 } 2013 2014 // Create all image elements 2015 const images = []; 2016 imagePaths.forEach(function(src, index) { 2017 const img = document.createElement('img'); 2018 img.src = src; 2019 img.alt = altText; 2020 img.loading = index === 0 ? 'eager' : 'lazy'; 2021 if (index === 0) { 2022 img.classList.add('is-active'); 2023 } 2024 container.appendChild(img); 2025 images.push(img); 2026 }); 2027 2028 // Height calculation removed - container height is now determined by CSS 2029 2030 // Simple fade slideshow
2031 let currentIndex = 0; 2032 function showNext() { 2033 images[currentIndex].classList.remove('is-active'); 2034 currentIndex = (currentIndex + 1) % images.length; 2035 images[currentIndex].classList.add('is-active'); 2036 } 2037 2038 // Start after 3 seconds, then change every 4 seconds 2039 setTimeout(function() { 2040 setInterval(showNext, 4000); 2041 }, 3000); 2042 }); 2043})(); 2044 2045// --- Social media caption truncation 2046(function(){ 2047 const maxLen = 700; 2048 document.querySelectorAll('.social-media__caption-text').forEach(function(el){ 2049 const text = (el.textContent || '').trim(); 2050 if (text.length > maxLen) { 2051 el.textContent = text.slice(0, maxLen).trimEnd() + '...'; 2052 } 2053 }); 2054})(); 2055 2056// --- Matomo: FlippingBook Publisher (embed.js / .fbp-embed) â Ãffnen & Download 2057(function () { 2058 var lastFbMs = {}; 2059 function trackFlippingBookMatomo(action, url) { 2060 if (typeof window._paq === 'undefined' || typeof window._paq.push !== 'function') return; 2061 if (!url || typeof url !== 'string') return; 2062 var base = url.split('#')[0].split('?')[0]; 2063 var key = action + '|' + base; 2064 var minGap = action === 'Download' ? 1500 : 900; 2065 var t = Date.now(); 2066 if (lastFbMs[key] && t - lastFbMs[key] < minGap) return; 2067 lastFbMs[key] = t; 2068 window._paq.push(['trackEvent', 'Magazin', action, base]); 2069 } 2070 2071 function isFlippingBookIframeSrc(src) { 2072 if (!src || src.indexOf('embed=true') === -1) return false; 2073 return /kleininteractive\.com|\/?ausgaben\//i.test(src); 2074 } 2075 2076 document.addEventListener('click', function (e) { 2077 var a = e.target && e.target.closest && e.target.closest('a.fb-embed-link, a.fbp-embed'); 2078 if (!a || !a.href) return; 2079 trackFlippingBookMatomo('Open', a.href); 2080 }, true); 2081 2082 window.addEventListener('message', function (ev) { 2083 var d = ev.data; 2084 if (!d || typeof d !== 'object' || typeof d.fbPublicationUrl !== 'string') return; 2085 var url = d.fbPublicationUrl; 2086 if (d.action === 'openMobileLightbox') { 2087 trackFlippingBookMatomo('Open', url); 2088 return; 2089 } 2090 if (d.action === 'closeLightbox') return; 2091 var act = String(d.action || ''); 2092 if (/download/i.test(act) || d.type === 'download' || d.download === true) { 2093 trackFlippingBookMatomo('Download', url); 2094 return; 2095 } 2096 if (d.event && /download/i.test(String(d.event))) { 2097 trackFlippingBookMatomo('Download', url); 2098 } 2099 }, false); 2100 2101 if (typeof MutationObserver === 'undefined') return; 2102 var iframeSeen = {}; 2103 function onIframe(el) { 2104 if (!el || el.tagName !== 'IFRAME') return; 2105 var src = el.src || el.getAttribute('src') || ''; 2106 if (!isFlippingBookIframeSrc(src)) return; 2107 var id = src.split('#')[0]; 2108 if (iframeSeen[id]) return; 2109 iframeSeen[id] = true; 2110 trackFlippingBookMatomo('Open', src); 2111 } 2112 function scanNode(n) { 2113 if (!n || n.nodeType !== 1) return; 2114 if (n.tagName === 'IFRAME') onIframe(n); 2115 var q = n.querySelectorAll && n.querySelectorAll('iframe'); 2116 if (!q) return; 2117 for (var i = 0; i < q.length; i++) onIframe(q[i]); 2118 } 2119 var mo = new MutationObserver(function (muts) { 2120 for (var i = 0; i < muts.length; i++) { 2121 var nodes = muts[i].addedNodes; 2122 for (var j = 0; j < nodes.length; j++) scanNode(nodes[j]); 2123 } 2124 }); 2125 var start = function () { 2126 if (document.body) mo.observe(document.body, { childList: true, subtree: true }); 2127 }; 2128 if (document.body) start(); 2129 else document.addEventListener('DOMContentLoaded', start); 2130})(); 2131// Personen-Profil: Foto-Slideshow im Hero 2132// Bullets, Autoplay, Swipe (Touch via Scroll-Snap), Klick blättert auf Geräten mit Maus weiter. 2133(function(){ 2134 function init(){ 2135 document.querySelectorAll('.person-profil-hero__slider').forEach(function(slider){ 2136 var scroller = slider.querySelector('.person-profil-hero__slides'); 2137 var bulletsWrap = slider.querySelector('.person-profil-hero__bullets'); 2138 if(!scroller) return; 2139 2140 var slides = Array.prototype.slice.call(scroller.children); 2141 if(slides.length < 2) return; 2142 2143 slider.classList.add('has-multiple'); 2144 2145 var current = 0; 2146 var bullets = []; 2147 2148 function goTo(i, behavior){ 2149 var n = slides.length; 2150 i = ((i % n) + n) % n; 2151 scroller.scrollTo({ left: i * scroller.clientWidth, behavior: behavior || 'smooth' }); 2152 } 2153 2154 if(bulletsWrap){
2155 slides.forEach(function(_, i){ 2156 var b = document.createElement('button'); 2157 b.type = 'button'; 2158 b.className = 'person-profil-hero__bullet' + (i === 0 ? ' is-active' : ''); 2159 b.setAttribute('aria-label', 'Foto ' + (i + 1) + ' von ' + slides.length); 2160 b.addEventListener('click', function(){ disableAutoplay(); goTo(i); }); 2161 bulletsWrap.appendChild(b); 2162 bullets.push(b); 2163 }); 2164 } 2165 2166 function setActive(i){ 2167 if(i === current) return; 2168 current = i; 2169 bullets.forEach(function(b, k){ b.classList.toggle('is-active', k === i); }); 2170 } 2171 2172 var scrollTimer = null; 2173 scroller.addEventListener('scroll', function(){ 2174 if(scrollTimer) clearTimeout(scrollTimer); 2175 scrollTimer = setTimeout(function(){ 2176 setActive(Math.round(scroller.scrollLeft / scroller.clientWidth)); 2177 }, 80); 2178 }, { passive: true }); 2179 2180 // Klick blättert weiter â nur auf Geräten ohne Touch (Maus/Trackpad) 2181 if(window.matchMedia('(hover: hover) and (pointer: fine)').matches){ 2182 scroller.addEventListener('click', function(){ disableAutoplay(); goTo(current + 1); }); 2183 } 2184 2185 // Autoplay: alle 5s weiter bis zum letzten Foto, dort endet es (kein Rücksprung 2186 // zum Anfang); pausiert auÃerhalb des Viewports / bei verstecktem Tab. 2187 // Sobald der Nutzer selbst blättert (Klick, Bullet, Swipe, Scrollrad), endet es dauerhaft. 2188 var AUTOPLAY_MS = 5000; 2189 var timer = null; 2190 var inView = true; 2191 var userTookOver = false; 2192 var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 2193 2194 function startAutoplay(){ 2195 if(reduceMotion || userTookOver || timer || !inView || document.hidden) return; 2196 if(current >= slides.length - 1) return; 2197 timer = setInterval(function(){ 2198 if(current >= slides.length - 1){ disableAutoplay(); return; } 2199 goTo(current + 1); 2200 }, AUTOPLAY_MS); 2201 } 2202 function stopAutoplay(){ if(timer){ clearInterval(timer); timer = null; } } 2203 function disableAutoplay(){ userTookOver = true; stopAutoplay(); } 2204 2205 if('IntersectionObserver' in window){ 2206 new IntersectionObserver(function(entries){ 2207 inView = entries[0].isIntersecting; 2208 if(inView){ startAutoplay(); } else { stopAutoplay(); } 2209 }, { threshold: 0.4 }).observe(slider); 2210 } 2211 document.addEventListener('visibilitychange', function(){ 2212 if(document.hidden){ stopAutoplay(); } else { startAutoplay(); } 2213 }); 2214 scroller.addEventListener('touchstart', disableAutoplay, { passive: true }); 2215 scroller.addEventListener('wheel', disableAutoplay, { passive: true }); 2216 2217 window.addEventListener('resize', function(){ goTo(current, 'auto'); }, { passive: true }); 2218 2219 startAutoplay(); 2220 }); 2221 } 2222 2223 if(document.readyState === 'loading'){ 2224 document.addEventListener('DOMContentLoaded', init); 2225 } else { 2226 init(); 2227 } 2228})();
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.