PageSourceSearch

https://media-atelier.tv/shared.js

js media-atelier.tv collected 2026-10-08 10:56:24 UTC 20,691 bytes, 546 lines download raw bytes

1/**
2 * Media Atelier – Shared JavaScript
3 * Common functionality for all pages: mobile menu, scroll reveal, icons, navbar scroll, PJAX router.
4 */
5
6// --- PJAX ROUTER ROUTING MAP & HELPERS ---
7const activeMappings = {
8    "index.html": "index.html",
9    "ueber-uns.html": "ueber-uns.html",
10    "leistungen.html": "leistungen.html",
11    "projekte.html": "projekte.html",
12    "tonstudio-mainz.html": "leistungen.html",
13    "postproduktion-mainz.html": "leistungen.html",
14    "hoerbuchproduktion.html": "leistungen.html",
15    "kontakt.html": "",
16    "impressum.html": "",
17    "datenschutz.html": ""
18};
19
20// Global scroll state tracker shared between navigateSPA and the scroll observer
21let lastScrollTop = 0;
22
23// --- PJAX PREFETCHING CACHE ---
24const prefetchCache = new Map();
25
26function prefetchPage(href) {
27    const resolvedUrl = new URL(href, window.location.href).href;
28    if (prefetchCache.has(resolvedUrl)) return;
29
30    const fetchPromise = fetch(resolvedUrl)
31        .then(response => {
32            if (!response.ok) throw new Error('Network response was not ok');
33            return response.text();
34        })
35        .catch(err => {
36            prefetchCache.delete(resolvedUrl);
37            console.error('Failed to prefetch page:', resolvedUrl, err);
38        });
39
40    prefetchCache.set(resolvedUrl, fetchPromise);
41}
42
43function getFilename(urlStr) {
44    if (!urlStr) return "";
45    let cleanUrl = urlStr.split('?')[0].split('#')[0];
46    let filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
47    return filename || "index.html";
48}
49
50function updateActiveNavLinks(href) {
51    const filename = getFilename(href);
52    const target = activeMappings[filename];
53    
54    // Highlight desktop nav links
55    const desktopLinks = document.querySelectorAll('header nav a');
56    desktopLinks.forEach(link => {
57        const linkHref = getFilename(link.getAttribute('href'));
58        if (target && linkHref === target) {
59            link.classList.add('active');
60        } else {
61            link.classList.remove('active');
62        }
63    });
64
65    // Highlight mobile nav links
66    const mobileLinks = document.querySelectorAll('#mobile-menu a');
67    mobileLinks.forEach(link => {
68        const linkHref = getFilename(link.getAttribute('href'));
69        if (target && linkHref === target) {
70            link.classList.remove('text-white');
71            link.classList.add('text-brand-yellow');
72        } else {
73            if (linkHref !== 'kontakt.html') {
74                link.classList.remove('text-brand-yellow');
75                link.classList.add('text-white');
76            }
77        }
78    });
79}
80
81function executePageScripts(doc) {
82    // 1. Clean up previous dynamic scripts to prevent duplicate executions/memory build up
83    document.querySelectorAll('.dynamic-script').forEach(el => el.remove());
84    
85    // 2. Fetch all script tags
86    const scripts = doc.querySelectorAll('script');
87    
88    // 3. Temporarily hook into addEventListener to capture DOMContentLoaded registrations
89    const originalAddEventListener = document.addEventListener;
90    const tempListeners = [];
91    document.addEventListener = function (type, listener, options) {
92        if (type === 'DOMContentLoaded') {
93            tempListeners.push(listener);
94        } else {
95            originalAddEventListener.call(document, type, listener, options);
96        }
97    };
98    
99    const originalWindowAddEventListener = window.addEventListener;
100    const tempWindowListeners = [];
101    window.addEventListener = function (type, listener, options) {
102        if (type === 'DOMContentLoaded') {
103            tempWindowListeners.push(listener);
104        } else {
105            originalWindowAddEventListener.call(window, type, listener, options);
106        }
107    };
108    
109    scripts.forEach(script => {
110        const src = script.getAttribute('src');
111        
112        // Skip globally loaded layout files
113        if (src && (
114            src.includes('shared.js') || 
115            src.includes('lucide') || 
116            src.includes('data.js') ||
117            src.includes('tailwind')
118        )) {
119            return;
120        }
121        
122        const newScript = document.createElement('script');
123        newScript.className = 'dynamic-script';
124        
125        if (src) {
126            newScript.src = src;
127            newScript.async = false;
128        } else {
129            newScript.textContent = script.textContent;
130        }
131        
132        document.body.appendChild(newScript);
133    });
134    
135    // Restore original event listeners
136    document.addEventListener = originalAddEventListener;
137    window.addEventListener = originalWindowAddEventListener;
138    
139    // Immediately execute captured DOMContentLoaded callbacks in order
140    tempListeners.forEach(fn => {
141        try { fn(); } catch (e) { console.error('Error in deferred DOMContentLoaded listener:', e); }
142    });
143    tempWindowListeners.forEach(fn => {
144        try { fn(); } catch (e) { console.error('Error in deferred window DOMContentLoaded listener:', e); }
145    });
146}
147
148/**
149 * Premium Parallelized SPA Router
150 * Concurrently triggers fade-out transition and fetch request, then swaps content smoothly
151 */
152function navigateSPA(href, pushToHistory = true) {
153    const currentMain = document.querySelector('main');
154    if (!currentMain) {
155        window.location.href = href;
156        return;
157    }
158
159    // 1. Trigger the main opacity fade-out immediately
160    currentMain.classList.add('main-fade-out');
161
162    // 2. Fetch the page HTML concurrently (check prefetch cache first)
163    const resolvedUrl = new URL(href, window.location.href).href;
164    let fetchPromise = prefetchCache.get(resolvedUrl);
165    if (!fetchPromise) {
166        fetchPromise = fetch(resolvedUrl)
167            .then(response => {
168                if (!response.ok) throw new Error('Network response was not ok');
169                return response.text();
170            });
171    }
172
173    // 3. Create a timer matching the CSS main fade-out duration (150ms)
174    const fadeOutPromise = new Promise(resolve => setTimeout(resolve, 150));
175
176    // 4. Swap content only when BOTH the fade-out completes and fetch returns
177    Promise.all([fetchPromise, fadeOutPromise])
178        .then(([htmlText]) => {
179            if (!htmlText) return;
180            
181            const parser = new DOMParser();
182            const doc = parser.parseFromString(htmlText, 'text/html');
183            
184            const newMain = doc.querySelector('main');
185            if (newMain && currentMain) {
186                // Pre-set matching classes and attributes
187                currentMain.className = newMain.className;
188                if (newMain.hasAttribute('style')) {
189                    currentMain.setAttribute('style', newMain.getAttribute('style'));
190                } else {
191                    currentMain.removeAttribute('style');
192                }
193                
194                // Ensure it remains faded out before innerHTML swap
195                currentMain.classList.add('main-fade-out');
196                
197                // Swap the innerHTML content
198                currentMain.innerHTML = newMain.innerHTML;
199            } else {
200                window.location.href = href;
201                return;
202            }
203            
204            document.title = doc.title;
205            updateActiveNavLinks(href);
206            
207            if (pushToHistory) {
208                history.pushState(null, '', href);
209            }
210            
211            // Reset intelligent header visibility & lastScrollTop state
212            const headerEl = document.querySelector('header');
213            if (headerEl) {
214                headerEl.classList.remove('header-hidden');
215                headerEl.classList.remove('header-scrolled');
216            }
217            lastScrollTop = 0; // Reset scroll calculations
218            
219            // Adjust scroll position before fading in
220            const hashIndex = href.indexOf('#');
221            if (hashIndex !== -1) {
222                const hash = href.substring(hashIndex);
223                try {
224                    const target = document.querySelector(hash);
225                    if (target) {
226                        target.scrollIntoView();
227                    } else {
228                        window.scrollTo(0, 0);
229                    }
230                } catch (err) {
231                    window.scrollTo(0, 0);
232                }
233            } else {
234                window.scrollTo(0, 0);
235            }
236            
237            // Close mobile menu if open
238            const mobileMenu = document.getElementById('mobile-menu');
239            if (mobileMenu) {
240                mobileMenu.classList.remove('menu-active');
241            }
242            
243            // Execute dynamic page scripts
244            executePageScripts(doc);
245            
246            // Re-observe scroll reveal elements in the new content
247            if (window.globalScrollObserver) {
248                document.querySelectorAll('.reveal-on-scroll').forEach(el => {
249                    window.globalScrollObserver.observe(el);
250                });
251            }
252            
253            // Re-render Lucide icons strictly scoped to the new main content (prevents document global scans)
254            if (typeof lucide !== 'undefined') {
255                lucide.createIcons();
256            }
257            
258            // Trigger smooth fade-in on the next frame
259            requestAnimationFrame(() => {
260                currentMain.classList.remove('main-fade-out');
261            });
262        })
263        .catch(err => {
264            console.error('PJAX load error, redirecting natively:', err);
265            window.location.href = href;
266        });
267}
268
269document.addEventListener('DOMContentLoaded', function () {
270    // --- INITIAL PAGE LOAD FADE-IN ---
271    const mainEl = document.querySelector('main');
272    if (mainEl) {
273        mainEl.style.opacity = '0';
274        mainEl.style.transform = 'translate3d(0, 10px, 0)';
275        mainEl.style.transition = 'none';
276        
277        requestAnimationFrame(() => {
278            mainEl.style.transition = 'opacity 0.4s cubic-bezier(0.25, 1, 0.5, 1), transform 0.4s cubic-bezier(0.25, 1, 0.5, 1)';
279            mainEl.style.opacity = '1';
280            mainEl.style.transform = 'translate3d(0, 0, 0)';
281            
282            // Clean up styles after transition to allow future SPA router page transitions
283            setTimeout(() => {
284                mainEl.style.transform = '';
285                mainEl.style.transition = '';
286            }, 450);
287        });
288    }
289
290    // --- MOBILE MENU ---
291    const menuBtn = document.getElementById('mobile-menu-btn');
292    const menu = document.getElementById('mobile-menu');
293    if (menuBtn && menu) {
294        // Remove static 'hidden' class to allow CSS transition styles to take control
295        menu.classList.remove('hidden');
296        
297        menuBtn.addEventListener('click', (e) => {
298            e.stopPropagation(); // Prevent document click handler from immediately closing it
299            menu.classList.toggle('menu-active');
300        });
301        
302        // Tap outside to close
303        document.addEventListener('click', (e) => {
304            if (menu.classList.contains('menu-active')) {
305                if (!menu.contains(e.target) && !menuBtn.contains(e.target)) {
306                    menu.classList.remove('menu-active');
307                }
308            }
309        });
310    }
311
312    // --- SCROLL REVEAL ---
313    const observerOptions = { root: null, rootMargin: '0px', threshold: 0.1 };
314    window.globalScrollObserver = new IntersectionObserver((entries) => {
315        entries.forEach(entry => {
316            if (entry.isIntersecting) {
317                entry.target.classList.add('reveal-visible');
318                window.globalScrollObserver.unobserve(entry.target);
319            }
320        });
321    }, observerOptions);
322
323    document.querySelectorAll('.reveal-on-scroll').forEach(el => window.globalScrollObserver.observe(el));
324
325    // --- INTELLIGENT NAVBAR SCROLL ---
326    const headerEl = document.querySelector('header');
327    const scrollThreshold = 80;
328    const minDelta = 6; // Avoid jitter from micro-scrolls
329
330    window.addEventListener('scroll', function () {
331        if (!headerEl) return;
332        let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
333        
334        // Don't hide header if mobile menu is currently open
335        const mobileMenuEl = document.getElementById('mobile-menu');
336        if (mobileMenuEl && mobileMenuEl.classList.contains('menu-active')) {
337            headerEl.classList.remove('header-hidden');
338            return;
339        }
340
341        // Apply glassmorphism / darker background when scrolled past 20px
342        if (scrollTop > 20) {
343            headerEl.classList.add('header-scrolled');
344        } else {
345            headerEl.classList.remove('header-scrolled');
346        }
347
348        // Calculate scroll delta & direction
349        const scrollDelta = scrollTop - lastScrollTop;
350
351        if (scrollTop <= 15) {
352            // Always display header at top of page
353            headerEl.classList.remove('header-hidden');
354        } else if (Math.abs(scrollDelta) > minDelta) {
355            if (scrollDelta > 0 && scrollTop > scrollThreshold) {
356                // Scrolling down -> hide navbar smoothly
357                headerEl.classList.add('header-hidden');
358            } else if (scrollDelta < 0) {
359                // Scrolling up -> reveal navbar smoothly
360                headerEl.classList.remove('header-hidden');
361            }
362        }
363
364        lastScrollTop = scrollTop <= 0 ? 0 : scrollTop;
365    }, { passive: true });
366
367    // --- LUCIDE ICONS ---
368    if (typeof lucide !== 'undefined') {
369        lucide.createIcons();
370    }
371
372    // --- AUDIO PLAYER LOGIC ---
373    let currentAudio = null;
374    document.addEventListener('click', function (e) {
375        const btn = e.target.closest('.play-btn');
376        if (!btn) return;
377        const url = btn.dataset.url;
378        if (!url) return;
379
380        if (currentAudio && currentAudio.src.includes(url) && !currentAudio.paused) {
381            currentAudio.pause();
382            btn.innerHTML = `<i data-lucide="play" class="w-4 h-4 fill-current"></i>`;
383            lucide.createIcons();
384        } else {
385            if (currentAudio) {
386                currentAudio.pause();
387                document.querySelectorAll('.play-btn').forEach(b => {
388                    b.innerHTML = `<i data-lucide="play" class="w-4 h-4 fill-current"></i>`;
389                    lucide.createIcons();
390                });
391            }
392            currentAudio = new Audio(url);
393            currentAudio.play();
394            btn.innerHTML = `<i data-lucide="pause" class="w-4 h-4 fill-current"></i>`;
395            lucide.createIcons();
396            currentAudio.onended = () => {
397                btn.innerHTML = `<i data-lucide="play" class="w-4 h-4 fill-current"></i>`;
398                lucide.createIcons();
399            };
400        }
401    });
402
403    // --- PAGE TRANSITIONS (PJAX INITIALIZATION) ---
404
405    // Handle back/forward navigation cleanly via PJAX
406    window.addEventListener('popstate', function () {
407        navigateSPA(window.location.href, false);
408    });
409
410    // Intercept clicks on local links for PJAX SPA transitions (main-only fade)
411    document.addEventListener('click', function (e) {
412        const link = e.target.closest('a');
413        if (!link) return;
414        
415        const href = link.getAttribute('href');
416        const target = link.getAttribute('target');
417        
418        // Skip external links, anchors, mailto/tel, and new tabs
419        if (!href || 
420            href.startsWith('mailto:') || 
421            href.startsWith('tel:') || 
422            target === '_blank' || 
423            link.hasAttribute('download') ||
424            e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
425            return;
426        }
427        
428        // Skip same-page anchors
429        if (href.startsWith('#')) {
430            return;
431        }
432        
433        // Verify if it is a local page
434        const isLocal = !href.includes('://') || href.includes(window.location.host);
435        if (isLocal) {
436            e.preventDefault();
437            navigateSPA(href);
438        }
439    });
440
441    // Prefetch links on mouse hover
442    document.addEventListener('mouseover', function (e) {
443        const link = e.target.closest('a');
444        if (!link) return;
445        
446        const href = link.getAttribute('href');
447        const target = link.getAttribute('target');
448        
449        if (!href || 
450            href.startsWith('mailto:') || 
451            href.startsWith('tel:') || 
452            href.startsWith('#') ||
453            target === '_blank' || 
454            link.hasAttribute('download')) {
455            return;
456        }
457        
458        const isLocal = !href.includes('://') || href.includes(window.location.host);
459        if (isLocal) {
460            prefetchPage(href);
461        }
462    });
463
464    // Prefetch links on mobile touch start (lowers click latency on mobile)
465    document.addEventListener('touchstart', function (e) {
466        const link = e.target.closest('a');
467        if (!link) return;
468        
469        const href = link.getAttribute('href');
470        const target = link.getAttribute('target');
471        
472        if (!href || 
473            href.startsWith('mailto:') || 
474            href.startsWith('tel:') || 
475            href.startsWith('#') ||
476            target === '_blank' || 
477            link.hasAttribute('download')) {
478            return;
479        }
480        
481        const isLocal = !href.includes('://') || href.includes(window.location.host);
482        if (isLocal) {
483            prefetchPage(href);
484        }
485    }, { passive: true });
486
487    // --- MEDIA SESSION API FOR MOBILE CONTROL CENTER ---
488    document.addEventListener('play', function (e) {
489        const audio = e.target;
490        if (!(audio instanceof HTMLAudioElement)) return;
491        
492        let title = "Hörprobe";
493        let artist = "Media Atelier Mainz";
494        
495        // Find parent player elements to extract semantic title/artist
496        const player = audio.closest('.compact-player');
497        if (player) {
498            const titleEl = player.querySelector('.cp-title');
499            if (titleEl) {
500                title = titleEl.textContent.replace("De-Hall", "").replace("Denoise", "").trim();
501                artist = "Audio-Restauration Beispiel";
502            }
503        } else {
504            // Find if there is a play button referencing this audio
505            // The file name will match the audio src filename
506            const urlParts = audio.src.split('/');
507            const filename = urlParts[urlParts.length - 1];
508            
509            const playBtn = document.querySelector(`.play-btn[data-url*="${filename}"]`);
510            if (playBtn) {
511                const speakerCard = playBtn.closest('.speaker-card') || playBtn.closest('.card-body-alt');
512                if (speakerCard) {
513                    const nameEl = speakerCard.querySelector('h3');
514                    if (nameEl) {
515                        artist = nameEl.textContent.trim();
516                        // Find label or name of the sample next to the button
517                        const labelEl = playBtn.previousElementSibling;
518                        if (labelEl) {
519                            title = `${labelEl.textContent.trim()} (Sprecher-Demo)`;
520                        } else {
521                            title = "Sprecher-Demo";
522                        }
523                    }
524                }
525            }
526        }
527        
528        if ('mediaSession' in navigator) {
529            navigator.mediaSession.metadata = new MediaMetadata({
530                title: title,
531                artist: artist,
532                album: 'Media Atelier Mainz',
533                artwork: [
534                    { src: 'Bilder/MA/m_hellgrau_rgb_small_wabe.webp', sizes: '256x256', type: 'image/png' }
535                ]
536            });
537            
538            navigator.mediaSession.setActionHandler('play', () => {
539                audio.play();
540            });
541            navigator.mediaSession.setActionHandler('pause', () => {
542                audio.pause();
543            });
544        }
545    }, true); // Use capture phase because play events do not bubble
546});

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.