PageSourceSearch

https://www.heiligenkreuz-lafnitztal.at/template2/assets/app.js

js heiligenkreuz-lafnitztal.at collected 2026-10-02 16:45:51 UTC 37,732 bytes, 903 lines download raw bytes

1(function () {
2    'use strict';
3
4    var COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
5    var TEXTSIZE_COOKIE = 'g24_a11y_textsize';
6    var CONTRAST_COOKIE = 'g24_a11y_contrast';
7
8    /* ========== Consent module ========== */
9    var g24Consent = (function () {
10        var KEY = 'g24_consent';
11
12        function getRaw() {
13            var m = document.cookie.match('(^|;)\\s*' + KEY + '\\s*=\\s*([^;]*)');
14            return m ? decodeURIComponent(m[2]) : null;
15        }
16        function setRaw(val) {
17            document.cookie = KEY + '=' + encodeURIComponent(val)
18                + '; max-age=' + COOKIE_MAX_AGE + '; path=/; SameSite=Lax';
19        }
20        function get() {
21            try { var r = getRaw(); return r ? JSON.parse(r) : null; } catch (e) { return null; }
22        }
23        function set(thirdparty) {
24            setRaw(JSON.stringify({ necessary: true, thirdparty: !!thirdparty }));
25        }
26        function hasConsented() { return get() !== null; }
27        function hasThirdparty() { var c = get(); return c ? !!c.thirdparty : false; }
28
29        return { get: get, set: set, hasConsented: hasConsented, hasThirdparty: hasThirdparty };
30    })();
31
32    document.addEventListener('DOMContentLoaded', function () {
33        initCallOnLoadLink();
34        repairBrokenAbsoluteMediaUrls();
35        repairEmbeddedIframes();
36        initWebcamImages();
37        initMobileMenu();
38        initMobileAccordions();
39        initContentAccordions();
40        initContextNavAccordions();
41        initDesktopMenuToggles();
42        initHeroSliders();
43        initSearchModal();
44        initA11yTools();
45        initCookieBanner();
46        gateExternalContent();
47        initLeafletMap();
48        initPhotoModal();
49    });
50
51    function initCallOnLoadLink() {
52        var link = document.querySelector('a.callonload[href]');
53        if (!link) { return; }
54
55        var href = String(link.getAttribute('href') || '').trim();
56        if (!href || href === '#') { return; }
57
58        // Prevent accidental self-navigation loops when alias points to current URL.
59        try {
60            var target = new URL(href, window.location.href);
61            if (target.href === window.location.href) {
62                return;
63            }
64            window.location.assign(target.href);
65            return;
66        } catch (e) {
67            // Fallback for malformed URLs: keep legacy behavior and try to navigate.
68        }
69
70        window.location.assign(href);
71    }
72
73    function repairBrokenAbsoluteMediaUrls() {
74        document.querySelectorAll('.wysiwyg-content [src], .wysiwyg-content [href]').forEach(function (el) {
75            ['src', 'href'].forEach(function (attr) {
76                if (!el.hasAttribute(attr)) { return; }
77
78                var value = el.getAttribute(attr) || '';
79                var repaired = normalizeBrokenAbsoluteUrl(value);
80                if (repaired !== value) {
81                    el.setAttribute(attr, repaired);
82                }
83            });
84        });
85    }
86
87    function normalizeBrokenAbsoluteUrl(value) {
88        return String(value || '')
89            .replace(/^(?:\.\.\/)+(?:admin\/)?(?=https?:\/\/)/i, '')
90            .replace(/^admin\/(?=https?:\/\/)/i, '');
91    }
92
93    function repairEmbeddedIframes() {
94        document.querySelectorAll('.wysiwyg-content iframe, .page-article__content iframe').forEach(function (iframe) {
95            var sandbox = iframe.getAttribute('sandbox');
96            if (sandbox !== null && sandbox.trim() === '') {
97                iframe.removeAttribute('sandbox');
98            }
99
100            var src = iframe.getAttribute('src') || '';
101            var normalizedSrc = normalizeEmbedUrl(src);
102            if (normalizedSrc !== src) {
103                iframe.setAttribute('src', normalizedSrc);
104            }
105        });
106    }
107
108    function normalizeEmbedUrl(value) {
109        var url = String(value || '');
110        if (url.indexOf('//') === 0) {
111            return window.location.protocol + url;
112        }
113
114        return url;
115    }
116
117    function initWebcamImages() {
118        var webcamImages = Array.prototype.slice.call(document.querySelectorAll('img.img-webcam[src]'));
119        if (!webcamImages.length) { return; }
120
121        webcamImages.forEach(function (img) {
122            if (!img.dataset.webcamBaseSrc) {
123                img.dataset.webcamBaseSrc = img.getAttribute('src') || '';
124            }
125        });
126
127        function refresh() {
128            var ts = Date.now();
129
130            webcamImages.forEach(function (img) {
131                var baseSrc = img.dataset.webcamBaseSrc || img.getAttribute('src') || '';
132                if (!baseSrc) { return; }
133
134                img.setAttribute('src', appendTimestamp(baseSrc, ts));
135            });
136        }
137
138        refresh();
139        window.setInterval(refresh, 15000);
140    }
141
142    function appendTimestamp(src, ts) {
143        try {
144            var url = new URL(src, window.location.href);
145            url.searchParams.set('ts', String(ts));
146            return url.toString();
147        } catch (e) {
148            var cleanSrc = String(src || '').replace(/([?&])ts=\d+(&)?/g, function (match, prefix, suffix) {
149                return prefix === '?' && suffix ? '?' : (suffix ? prefix : '');
150            }).replace(/[?&]$/, '');
151            return cleanSrc + (cleanSrc.indexOf('?') === -1 ? '?' : '&') + 'ts=' + ts;
152        }
153    }
154
155    /* ========== Mobile menu ========== */
156    function initMobileMenu() {
157        var openBtn = document.getElementById('mobile-btn');
158        var closeBtn = document.getElementById('mobile-close');
159        var panel = document.getElementById('mobile-menu');
160        var overlay = document.getElementById('mobile-overlay');
161        var lastTrigger = null;
162
163        if (!openBtn || !panel || !overlay) { return; }
164
165        function setState(open) {
166            panel.classList.toggle('is-open', open);
167            overlay.classList.toggle('is-open', open);
168            panel.setAttribute('aria-hidden', open ? 'false' : 'true');
169            openBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
170            document.body.style.overflow = open ? 'hidden' : '';
171            if (open) {
172                lastTrigger = document.activeElement;
173                if (closeBtn) { closeBtn.focus(); }
174            } else if (lastTrigger && typeof lastTrigger.focus === 'function') {
175                lastTrigger.focus();
176            }
177        }
178
179        openBtn.addEventListener('click', function () { setState(true); });
180        if (closeBtn) { closeBtn.addEventListener('click', function () { setState(false); }); }
181        overlay.addEventListener('click', function () { setState(false); });
182        document.addEventListener('keydown', function (e) {
183            if (e.key === 'Escape') { setState(false); }
184        });
185    }
186
187    /* ========== Content accordions (.accordion-trigger) ========== */
188    function initContentAccordions() {
189        document.querySelectorAll('.accordion-trigger:not([data-acc-init])').forEach(function (trigger) {
190            var panel = trigger.closest('.accordion-item')
191                ? trigger.closest('.accordion-item').querySelector('.accordion-panel')
192                : null;
193            trigger.dataset.accInit = '1';
194            trigger.addEventListener('click', function () {
195                var expanded = trigger.getAttribute('aria-expanded') === 'true';
196                trigger.setAttribute('aria-expanded', expanded ? 'false' : 'true');
197                if (panel) { panel.classList.toggle('is-open', !expanded); }
198            });
199        });
200    }
201
202    function initMobileAccordions() {
203        var triggers = document.querySelectorAll('.mobile-acc-trigger');
204        triggers.forEach(function (trigger) {
205            trigger.addEventListener('click', function () {
206                var contentId = trigger.getAttribute('aria-controls');
207                var content = contentId ? document.getElementById(contentId) : null;
208                var open = trigger.classList.toggle('is-open');
209                trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
210                if (content) { content.classList.toggle('is-open', open); }
211            });
212        });
213    }
214
215    function initContextNavAccordions() {
216        var toggles = document.querySelectorAll('.context-nav__toggle:not([data-context-nav-init])');
217        toggles.forEach(function (toggle) {
218            var contentId = toggle.getAttribute('aria-controls');
219            var content = contentId ? document.getElementById(contentId) : null;
220            var item = toggle.closest('.context-nav__item');
221
222            if (!content || !item) { return; }
223
224            toggle.dataset.contextNavInit = '1';
225            item.classList.toggle('is-expanded', !content.hidden);
226            item.classList.toggle('is-collapsed', content.hidden);
227
228            toggle.addEventListener('click', function () {
229                var open = toggle.getAttribute('aria-expanded') === 'true';
230                toggle.setAttribute('aria-expanded', open ? 'false' : 'true');
231                content.hidden = open;
232                item.classList.toggle('is-expanded', !open);
233                item.classList.toggle('is-collapsed', open);
234            });
235        });
236    }
237
238    function initDesktopMenuToggles() {
239        var items = document.querySelectorAll('.nav-item.has-children, .sub-item.has-children');
240        items.forEach(function (item) {
241            var trigger = item.querySelector(':scope > .nav-top-link--toggle, :scope > .nav-link--toggle');
242            if (!trigger) { return; }
243            function syncExpanded() {
244                trigger.setAttribute('aria-expanded', item.matches(':hover, :focus-within') ? 'true' : 'false');
245            }
246            item.addEventListener('mouseenter', syncExpanded);
247            item.addEventListener('mouseleave', syncExpanded);
248            item.addEventListener('focusin', syncExpanded);
249            item.addEventListener('focusout', function () {
250                window.setTimeout(syncExpanded, 0);
251            });
252            syncExpanded();
253        });
254    }
255
256    /* ========== Hero sliders ========== */
257    function initHeroSliders() {
258        var sliders = document.querySelectorAll('[data-slider]');
259        sliders.forEach(function (slider) {
260            var slides = slider.querySelectorAll('[data-slide]');
261            var dots = slider.querySelectorAll('[data-slide-to]');
262            if (slides.length < 2) { return; }
263            var current = 0;
264            var interval = null;
265
266            function render(idx) {
267                slides.forEach(function (slide, i) {
268                    slide.classList.toggle('is-active', i === idx);
269                });
270                dots.forEach(function (dot, i) {
271                    dot.classList.toggle('is-active', i === idx);
272                });
273                current = idx;
274            }
275            function start() {
276                clearInterval(interval);
277                interval = setInterval(function () {
278                    render((current + 1) % slides.length);
279                }, 6000);
280            }
281            dots.forEach(function (dot) {
282                dot.addEventListener('click', function () {
283                    var idx = parseInt(dot.getAttribute('data-slide-to') || '0', 10);
284                    if (!isNaN(idx)) { render(idx); start(); }
285                });
286            });
287            slider.addEventListener('mouseenter', function () { clearInterval(interval); });
288            slider.addEventListener('mouseleave', start);
289            render(0);
290            start();
291        });
292    }
293
294    /* ========== Search modal ========== */
295    function initSearchModal() {
296        var modal = document.getElementById('search-modal');
297        var btn = document.getElementById('search-btn');
298        var closeBtn = document.getElementById('search-close');
299        var input = document.getElementById('search-input');
300        var results = document.getElementById('search-results');
301        var lastTrigger = null;
302
303        if (!modal || !btn || !input || !results) { return; }
304
305        var entries = mergeSearchEntries(collectSearchEntries(), Array.isArray(window.G24_SEARCH_ENTRIES) ? window.G24_SEARCH_ENTRIES : []);
306
307        function renderResults(query) {
308            var q = normalizeSearchValue(query);
309            var filtered = q
310                ? entries.filter(function (e) {
311                    var haystack = normalizeSearchValue((e.search || '') + ' ' + (e.label || '') + ' ' + (e.text || '') + ' ' + (e.group || ''));
312                    return haystack.indexOf(q) !== -1;
313                }
313).slice(0, 30)
314                : entries.slice(0, 20);
315            if (!filtered.length) {
316                results.innerHTML = '<p class="search-empty">Keine Ergebnisse</p>';
317                return;
318            }
319            var groups = {};
320            filtered.forEach(function (e) {
321                if (!groups[e.group]) { groups[e.group] = []; }
322                groups[e.group].push(e);
323            });
324            var html = '';
325            Object.keys(groups).forEach(function (group) {
326                html += '<div class="search-group">';
327                html += '<p class="search-group__label">' + escapeHtml(group) + '</p>';
328                groups[group].forEach(function (entry) {
329                    html += '<a href="' + escapeAttr(entry.url) + '">';
330                    html += '<span class="search-result__title">' + escapeHtml(entry.label) + '</span>';
331                    if (entry.text) {
332                        html += '<span class="search-result__text">' + escapeHtml(entry.text) + '</span>';
333                    }
334                    html += '</a>';
335                });
336                html += '</div>';
337            });
338            results.innerHTML = html;
339        }
340
341        function open() {
342            modal.classList.add('is-open');
343            modal.setAttribute('aria-hidden', 'false');
344            btn.setAttribute('aria-expanded', 'true');
345            lastTrigger = document.activeElement;
346            setTimeout(function () { input.focus(); }, 50);
347            renderResults('');
348        }
349        function close() {
350            modal.classList.remove('is-open');
351            modal.setAttribute('aria-hidden', 'true');
352            btn.setAttribute('aria-expanded', 'false');
353            input.value = '';
354            if (lastTrigger && typeof lastTrigger.focus === 'function') {
355                lastTrigger.focus();
356            }
357        }
358
359        btn.addEventListener('click', open);
360        if (closeBtn) { closeBtn.addEventListener('click', close); }
361        modal.addEventListener('click', function (e) { if (e.target === modal) { close(); } });
362        input.addEventListener('input', function (e) { renderResults(e.target.value); });
363        document.addEventListener('keydown', function (e) {
364            if ((e.metaKey || e.ctrlKey) && e.key && e.key.toLowerCase() === 'k') {
365                e.preventDefault();
366                modal.classList.contains('is-open') ? close() : open();
367            }
368            if (e.key === 'Escape') { close(); }
369        });
370    }
371
372    function collectSearchEntries() {
373        var entries = [];
374        var root = document.querySelector('.desktop-nav .nav-root');
375        if (!root) { return entries; }
376        var topItems = root.querySelectorAll(':scope > .nav-item');
377        topItems.forEach(function (top) {
378            var topLabel = textOf(top.querySelector(':scope > .nav-top-link'));
379            var topUrl = hrefOf(top.querySelector(':scope > a.nav-top-link')) || '#';
380            entries.push({ label: topLabel, url: topUrl, group: 'Hauptnavigation' });
381            top.querySelectorAll(':scope > .nav-dropdown .nav-submenu-inner > .sub-item').forEach(function (sub) {
382                var subLabel = textOf(sub.querySelector(':scope > .nav-link, :scope > .nav-link--toggle'));
383                var subUrl = hrefOf(sub.querySelector(':scope > a.nav-link')) || '#';
384                entries.push({ label: topLabel + ' › ' + subLabel, url: subUrl, group: topLabel });
385                sub.querySelectorAll(':scope > .nav-flyout .nav-submenu-inner > .sub-item > a.nav-link').forEach(function (leaf) {
386                    entries.push({ label: topLabel + ' › ' + subLabel + ' › ' + leaf.textContent.trim(), url: leaf.getAttribute('href') || '#', group: topLabel });
387                });
388            });
389        });
390        return entries;
391    }
392
393    function mergeSearchEntries(navEntries, contentEntries) {
394        var seen = new Set();
395
396        return navEntries.concat(contentEntries).map(function (entry) {
397            return {
398                label: entry && (entry.label || entry.title) ? String(entry.label || entry.title) : '',
399                text: entry && entry.text ? String(entry.text) : '',
400                search: entry && entry.search ? String(entry.search) : '',
401                group: entry && entry.group ? String(entry.group) : 'Weitere Inhalte',
402                url: entry && entry.url ? String(entry.url) : ''
403            };
404        }).filter(function (entry) {
405            if (!entry.label || !entry.url) {
406                return false;
407            }
408
409            var key = [entry.group || '', entry.label || '', entry.url || ''].join('||');
410            if (seen.has(key)) {
411                return false;
412            }
413            seen.add(key);
414
415            return true;
416        });
417    }
418
419    function normalizeSearchValue(value) {
420        return String(value || '')
421            .toLowerCase()
422            .normalize('NFD')
423            .replace(/[\u0300-\u036f]/g, '')
424            .replace(/ä/g, 'a')
425            .replace(/ö/g, 'o')
426            .replace(/ü/g, 'u')
427            .replace(/ß/g, 'ss')
428            .trim();
429    }
430
431    function textOf(el) { return el ? (el.textContent || '').trim() : ''; }
432    function hrefOf(el) { return el ? el.getAttribute('href') : null; }
433
434    /* ========== A11y tools ========== */
435    function initA11yTools() {
436        var textBtn = document.querySelector('[data-a11y-textsize]');
437        var contrastBtn = document.querySelector('[data-a11y-contrast]');
438        var root = document.documentElement;
439        var textSizeLevel = clampTextSizeLevel(readCookie(TEXTSIZE_COOKIE));
440        var contrastEnabled = readCookie(CONTRAST_COOKIE) === '1';
441
442        function applyTextSize(level) {
443            textSizeLevel = clampTextSizeLevel(level);
444            if (textSizeLevel > 0) {
445                root.setAttribute('data-a11y-textsize', String(textSizeLevel));
446            } else {
447                root.removeAttribute('data-a11y-textsize');
448            }
449            writeCookie(TEXTSIZE_COOKIE, String(textSizeLevel));
450            if (textBtn) {
451                textBtn.setAttribute('aria-pressed', textSizeLevel > 0 ? 'true' : 'false');
452                textBtn.setAttribute('aria-label', 'Schriftgrösse vergrössern, Stufe ' + textSizeLevel + ' von 3');
453            }
454        }
455
456        function applyContrast(enabled) {
457            contrastEnabled = !!enabled;
458            document.body.classList.toggle('a11y-contrast', contrastEnabled);
459            writeCookie(CONTRAST_COOKIE, contrastEnabled ? '1' : '0');
460            if (contrastBtn) {
461                contrastBtn.setAttribute('aria-pressed', contrastEnabled ? 'true' : 'false');
462            }
463        }
464
465        applyTextSize(textSizeLevel);
466        applyContrast(contrastEnabled);
467
468        if (textBtn) {
469            textBtn.addEventListener('click', function () {
470                applyTextSize((textSizeLevel + 1) % 4);
471            });
472        }
473        if (contrastBtn) {
474            contrastBtn.addEventListener('click', function () {
475                applyContrast(!contrastEnabled);
476            });
477        }
478    }
479
480    /* ========== Shared consent helpers ========== */
481    function showCookieBanner() {
482        var b = document.getElementById('cookie-banner');
483        if (b) { b.hidden = false; }
484    }
485    function hideCookieBanner() {
486        var b = document.getElementById('cookie-banner');
487        if (b) { b.hidden = true; }
488    }
489    function syncBannerToggle(checked) {
490        var t = document.getElementById('cookie-toggle-thirdparty');
491        if (t) { t.checked = checked; }
492    }
493    /* Called when thirdparty consent is granted — activates all gated content */
494    function onThirdpartyGranted() {
495        hideCookieBanner();
496        syncBannerToggle(true);
497        gateExternalContent(); // re-run: will now activate since consent is true
498        initLeafletMap();
499    }
500
501    /* ========== Cookie banner ========== */
502    function initCookieBanner() {
503        var banner = document.getElementById('cookie-banner');
504        if (!banner) { return; }
505
506        var toggle  = document.getElementById('cookie-toggle-thirdparty');
507        var btnNec  = document.getElementById('cookie-btn-necessary');
508        var btnSave = document.getElementById('cookie-btn-save');
509        var btnAll  = document.getElementById('cookie-btn-all');
510
511        function showBanner() {
512            banner.hidden = false;
513            banner.focus();
514        }
515
516        function applyConsent(thirdparty) {
517            g24Consent.set(thirdparty);
518            hideCookieBanner();
519            syncBannerToggle(thirdparty);
520            requestHomepageCacheClear();
521            gateExternalContent();
522            if (thirdparty) { initLeafletMap(); }
523        }
524
525        // Sync toggle to stored consent state
526        var existing = g24Consent.get();
527        if (toggle && existing) { toggle.checked = !!existing.thirdparty; }
528
529        // Auto-show only if admin enabled it and no consent given yet
530        var autoShow = banner.getAttribute('data-auto-show') === '1';
531        if (autoShow && !g24Consent.hasConsented()) { showBanner(); }
532
533        if (btnNec)  { btnNec.addEventListener('click',  function () { applyConsent(false); }); }
534        if (btnSave) { btnSave.addEventListener('click',  function () { applyConsent(toggle ? toggle.checked : false); }); }
535        if (btnAll)  { btnAll.addEventListener('click',   function () { applyConsent(true); }); }
536
537        // Re-open triggers (footer + datenschutz page) via event delegation
538        document.addEventListener('click', function (e) {
539            var t = e.target;
540            if (t && (t.id === 'cookie-reopen-btn' || t.id === 'datenschutz-cookie-reopen' || t.id === 'datenschutz-cookie-settings-btn')) {
541                e.preventDefault();
542                // Sync toggle to current state before showing
543                var c = g24Consent.get();
544                if (toggle && c) { toggle.checked = !!c.thirdparty; }
545                showBanner();
546                banner.scrollIntoView({ behavior: 'smooth', block: 'end' });
547            }
548        });
549    }
550
551    function requestHomepageCacheClear() {
552        var url = '/_homepage/cache/clear';
553
554        if (navigator.sendBeacon) {
555            try {
556                navigator.sendBeacon(url, new Blob([], { type: 'text/plain' }));
557                return;
558            } catch (e) {
559                // fall through to fetch
560            }
561        }
562
563        if (window.fetch) {
564            fetch(url, {
565                method: 'POST',
566                credentials: 'same-origin',
567                keepalive: true,
568                headers: {
569                    'X-Requested-With': 'XMLHttpRequest'
570                }
571            }).catch(function () {});
572        }
573    }
574
575    /* ========== External content gating (iframes in page content) ========== */
576    function gateExternalContent() {
577        // 1. Activate already-gated iframes if consent now given
578        if (g24Consent.hasThirdparty()) {
579            document.querySelectorAll('iframe[data-orig-src]').forEach(function (f) {
580                f.src = normalizeEmbedUrl(f.getAttribute('data-orig-src'));
581                f.removeAttribute('data-orig-src');
582                f.style.display = '';
583            });
584            document.querySelectorAll('.ext-consent-gate').forEach(function (g) { g.remove(); });
585            return;
586        }
587
588        // 2. Find external iframes not yet gated
589        var areas = document.querySelectorAll('.wysiwyg-content, .page-article__content');
590        areas.forEach(function (area) {
591            area.querySelectorAll('iframe:not([data-orig-src])').forEach(function (iframe) {
592                var src = normalizeEmbedUrl(iframe.getAttribute('src') || '');
593                if (!src || src === 'about:blank') { return; }
594                if (src.charAt(0) === '/' && src.charAt(1) !== '/') { return; }
595
596                // Determine service label
597                var label = src.indexOf('youtube') >= 0 || src.indexOf('youtu.be') >= 0 ? 'YouTube'
598                    : src.indexOf('vimeo') >= 0 ? 'Vimeo'
599                    : src.indexOf('maps.google') >= 0 ? 'Google Maps'
600                    : src.indexOf('soundcloud') >= 0 ? 'SoundCloud'
601                    : 'Externer Inhalt';
602
603                // Save src and blank the iframe
604                iframe.setAttribute('data-orig-src', src);
605                iframe.src = 'about:blank';
606                iframe.style.display = 'none';
607
608                // Build gate placeholder in place of the iframe
609                var gate = document.createElement('div');
610                gate.className = 'ext-consent-gate';
611                gate.innerHTML =
612                    '<div class="ext-consent-gate__overlay">'
613                    + '<svg class="ext-consent-gate__icon" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>'
614                    + '<p class="ext-consent-gate__label">' + escapeHtml(label) + '</p>'
615                    + '<p class="ext-consent-gate__title">Inhalt nicht geladen</p>'
616                    + '<p class="ext-consent-gate__text">Dieser Inhalt wird von <strong>' + escapeHtml(label) + '</strong> bereitgestellt. Um ihn anzuzeigen, müssen Drittanbieter-Dienste aktiviert werden.</p>'
617                    + '<button type="button" class="ext-consent-gate__btn">Inhalt aktivieren</button>'
618                    + '<a href="/datenschutz" class="ext-consent-gate__link">Datenschutz</a>'
619                    + '</div>';
620                iframe.parentNode.insertBefore(gate, iframe);
621
622                gate.querySelector('.ext-consent-gate__btn').addEventListener('click', function () {
623                    g24Consent.set(true);
624                    onThirdpartyGranted();
625                });
626            });
627        });
628    }
629
630    /* ========== Leaflet map (consent-aware) ========== */
631    function initLeafletMap() {
632        var el = document.getElementById('map');
633        if (!el) { return; }
634
635        var lat = parseFloat(el.getAttribute('data-lat') || '');
636        var lng = parseFloat(el.getAttribute('data-lng') || '');
637        var label = el.getAttribute('data-label') || '';
638        if (isNaN(lat) || isNaN(lng)) { return; }
639
640        // Already initialized (Leaflet stamps _leaflet_id on the container)
641        if (el._leaflet_id) { return; }
642
643        // No consent → show overlay inside the map card (parent of canvas)
644        if (!g24Consent.hasThirdparty()) {
645            var card = el.closest('.map-card') || el.parentElement;
646            if (card && card.querySelector('.map-consent-overlay')) { return; }
647            var overlay = document.createElement('div');
648            overlay.className = 'map-consent-overlay';
649            overlay.innerHTML =
650                '<svg class="map-consent-overlay__icon" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
651                + '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>'
652                + '<p class="map-consent-overlay__title">Karte nicht geladen</p>'
653                + '<p class="map-consent-overlay__text">Diese Karte lädt Daten von <strong>OpenStreetMap</strong> (Drittanbieter). Aktivieren Sie den Dienst, um die Karte anzuzeigen.</p>'
654                + '<button type="button" class="map-consent-overlay__btn">Karte aktivieren</button>'
655                + '<a href="/datenschutz" class="map-consent-overlay__link">Datenschutz</a>';
656            (card || el).appendChild(overlay);
657            overlay.querySelector('.map-consent-overlay__btn').addEventListener('click', function () {
658                g24Consent.set(true);
659                onThirdpartyGranted();
660            });
661            return;
662        }
663
664        // Consent given — wait for Leaflet library if still loading (defer)
665        if (typeof L === 'undefined') {
666            setTimeout(initLeafletMap, 200);
667            return;
668        }
669
670        // Remove consent overlay if present
671        var card = el.closest('.map-card') || el.parentElement;
672        var prev = card ? card.querySelector('.map-consent-overlay') : null;
673        if (prev) { prev.remove(); }
674
675        var map = L.map(el, { zoomControl: true, scrollWheelZoom: false }).setView([lat, lng], 16);
676        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
677            attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
678            maxZoom: 19
679        }).addTo(map);
680
681        var accent = getComputedStyle(document.documentElement).getPropertyValue('--theme-accent').trim() || '#0f766e';
682        var icon = L.divIcon({
683            className: '',
684            html: '<div style="width:28px;height:28px;border-radius:50%;background:' + accent + ';border:3px solid white;box-shadow:0 4px 12px rgba(0,0,0,.3)"></div>',
685            iconSize: [28, 28],
686            iconAnchor: [14, 14]
687        });
688        L.marker([lat, lng], { icon: icon }).addTo(map).bindPopup(label || '');
689    }
690
691    /* ========== helpers ========== */
692    function escapeHtml(s) {
693        return String(s || '').replace(/[&<>"']/g, function (c) {
694            return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
695        });
696    }
697    /* ========== Photo modal ========== */
698    function initPhotoModal() {
699        var modal = document.getElementById('photo-modal');
700        if (!modal) { return; }
701        var img = document.getElementById('photo-modal-img');
702        var caption = document.getElementById('photo-modal-caption');
703        var counter = document.getElementById('photo-modal-counter');
704        var closeBtn = modal.querySelector('.photo-modal__close');
705        var backdrop = modal.querySelector('.photo-modal__backdrop');
706        var prevBtn = modal.querySelector('.photo-modal__nav--prev');
707        var nextBtn = modal.querySelector('.photo-modal__nav--next');
708        var currentGroup = [];
709        var currentIndex = 0;
710        var lastTrigger = null;
711
712        function getPhotoName(wrapper, image) {
713            var card = wrapper ? wrapper.closest('.contact-card, .contact-inline') : null;
714            var nameEl = card ? card.querySelector('.contact-card__name, .contact-inline__name') : null;
715            var roleEl = card ? card.querySelector('.contact-card__role, .contact-inline__role') : null;
716            var name = nameEl ? nameEl.textContent.trim() : '';
717
718            if (!name && image) {
719                name = (image.getAttribute('alt') || '').trim();
720            }
721
722            if (!name && roleEl) {
723                name = roleEl.textContent.trim();
724            }
725
726            return name;
727        }
728
729        function prepareContactPhotoTriggers() {
730            document.querySelectorAll('.contact-card__avatar, .contact-inline__avatar').forEach(function (wrapper) {
731                if (wrapper.hasAttribute('data-photo-modal')) {
732                    return;
733                }
734
735                var image = wrapper.querySelector('img');
736                var src = image ? (image.currentSrc || image.getAttribute('src') || '') : '';
737
738                if (!src) {
739                    return;
740                }
741
742                var name = getPhotoName(wrapper, image);
743
744                wrapper.dataset.photoModal = src;
745                if (name !== '') {
746                    wrapper.dataset.photoName = name;
747                    wrapper.setAttribute('aria-label', 'Foto von ' + name + ' vergrößern');
748                }
749
750                if (wrapper.classList.contains('contact-card__avatar')) {
751                    wrapper.classList.add('contact-card__avatar--clickable');
752                }
753                if (wrapper.classList.contains('contact-inline__avatar')) {
754                    wrapper.classList.add('contact-inline__avatar--clickable');
755                }
756
757                wrapper.setAttribute('role', 'button');
758                wrapper.setAttribute('tabindex', '0');
759            });
760        }
761
762        function resolvePhotoGroup(trigger) {
763            var groupRoot = trigger ? trigger.closest('[data-photo-gallery]') : null;
764            if (!groupRoot) {
765                return [trigger];
766            }
767
768            return Array.prototype.slice.call(groupRoot.querySelectorAll('[data-photo-modal]'));
769        }
770
771        function renderCurrentPhoto() {
772            var current = currentGroup[currentIndex];
773            if (!current) {
774                return;
775            }
776
777            var name = current.dataset.photoName || '';
778            var description = current.dataset.photoDescription || '';
779
780            img.src = current.dataset.photoModal || '';
781            img.alt = name;
782            caption.textContent = description !== '' ? (name !== '' ? name + ' - ' + description : description) : name;
783
784            if (counter) {
785                if (currentGroup.length > 1) {
786                    counter.hidden = false;
787                    counter.textContent = String(currentIndex + 1) + ' / ' + String(currentGroup.length);
788                } else {
789                    counter.hidden = true;
790                    counter.textContent = '';
791                }
792            }
793
794            if (prevBtn) {
795                prevBtn.hidden = currentGroup.length <= 1;
796            }
797            if (nextBtn) {
798                nextBtn.hidden = currentGroup.length <= 1;
799            }
800        }
801
802        function openTrigger(trigger) {
803            currentGroup = resolvePhotoGroup(trigger).filter(function (item) {
804                return item && item.dataset && item.dataset.photoModal;
805            });
806
807            if (currentGroup.length === 0) {
808                currentGroup = [trigger];
809            }
810
811            currentIndex = Math.max(0, currentGroup.indexOf(trigger));
812            lastTrigger = trigger;
813            renderCurrentPhoto();
814            modal.hidden = false;
815            document.body.style.overflow = 'hidden';
816            closeBtn.focus();
817        }
818
819        function stepPhoto(direction) {
820            if (currentGroup.length <= 1) {
821                return;
822            }
823
824            currentIndex = (currentIndex + direction + currentGroup.length) % currentGroup.length;
825            renderCurrentPhoto();
826        }
827
828        function closeModal() {
829            modal.hidden = true;
830            document.body.style.overflow = '';
831            img.src = '';
832            img.alt = '';
833            caption.textContent = '';
834            if (counter) {
835                counter.hidden = true;
836                counter.textContent = '';
837            }
838            if (prevBtn) { prevBtn.hidden = true; }
839            if (nextBtn) { nextBtn.hidden = true; }
840            if (lastTrigger && typeof lastTrigger.focus === 'function') {
841                lastTrigger.focus();
842            }
843        }
844
845        prepareContactPhotoTriggers();
846
847        document.addEventListener('click', function (e) {
848            var trigger = e.target.closest('[data-photo-modal]');
849            if (trigger) {
850                e.preventDefault();
851                openTrigger(trigger);
852            }
853        });
854
855        if (closeBtn) { closeBtn.addEventListener('click', closeModal); }
856        if (backdrop) { backdrop.addEventListener('click', closeModal); }
857        if (prevBtn) {
858            prevBtn.addEventListener('click', function () { stepPhoto(-1); });
859        }
860        if (nextBtn) {
861            nextBtn.addEventListener('click', function () { stepPhoto(1); });
862        }
863        document.addEventListener('keydown', function (e) {
864            var activeTrigger = document.activeElement;
865            if ((e.key === 'Enter' || e.key === ' ') && activeTrigger && activeTrigger.matches('[data-photo-modal][role="button"]')) {
866                e.preventDefault();
867                openTrigger(activeTrigger);
868                return;
869            }
870            if (e.key === 'ArrowLeft' && !modal.hidden) {
871                e.preventDefault();
872                stepPhoto(-1);
873                return;
874            }
875            if (e.key === 'ArrowRight' && !modal.hidden) {
876                e.preventDefault();
877                stepPhoto(1);
878                return;
879            }
880            if (e.key === 'Escape' && !modal.hidden) { closeModal(); }
881        });
882    }
883
884    function escapeAttr(s) { return escapeHtml(s); }
885
886    function clampTextSizeLevel(value) {
887        var parsed = parseInt(value || '0', 10);
888        if (isNaN(parsed) || parsed < 0) { return 0; }
889        if (parsed > 3) { return 3; }
890        return parsed;
891    }
892
893    function readCookie(name) {
894        var escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
895        var match = document.cookie.match(new RegExp('(?:^|; )' + escapedName + '=([^;]*)'));
896        return match ? decodeURIComponent(match[1]) : '';
897    }
898
899    function writeCookie(name, value) {
900        document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; max-age=' + COOKIE_MAX_AGE + '; SameSite=Lax';
901    }
902
903})();

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.