// OmégaKube - shared scripts // Feather icons loader (centralized) // loads https://unpkg.com/feather-icons when needed and exposes window.ensureFeather() const FEATHER_URL = 'https://unpkg.com/feather-icons'; function ensureFeather() { if (window.__featherLoading) return window.__featherLoading; if (window.feather && window.feather.replace) return Promise.resolve(window.feather); window.__featherLoading = new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = FEATHER_URL; s.async = true; s.onload = () => resolve(window.feather); s.onerror = () => reject(new Error('Failed to load feather-icons')); document.head.appendChild(s); }); return window.__featherLoading; } window.ensureFeather = ensureFeather; document.addEventListener('DOMContentLoaded', async () => { // ensure feather icons are available and replace icons in document try { await ensureFeather(); if (window.feather && feather.replace) try { feather.replace(); } catch(e) {} } catch (e) {} // Load HTML includes (components header/footer) and fix relative paths when necessary async function loadIncludes() { const includeSelector = '[data-include]'; const nodes = Array.from(document.querySelectorAll(includeSelector)); if (!nodes.length) return; await Promise.all(nodes.map(async el => { const url = el.getAttribute('data-include'); try { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); const text = await res.text(); const template = document.createElement('template'); template.innerHTML = text; const fragment = template.content; // adjust relative paths for assets and page links when included from /pages/ fragment.querySelectorAll('[href], [src]').forEach(node => { ['href','src'].forEach(attr => { if (!node.hasAttribute(attr)) return; const original = node.getAttribute(attr); // leave absolute URLs and anchors untouched if (/^(https?:|\/|#)/.test(original)) return; // compute useful bases const includeUrl = new URL(url, document.baseURI); const includeDir = includeUrl.pathname.substring(0, includeUrl.pathname.lastIndexOf('/') + 1); const docUrl = new URL(document.location.href); const docDir = docUrl.pathname.substring(0, docUrl.pathname.lastIndexOf('/') + 1); // find common prefix between include location and current document to infer site root const includeParts = includeDir.split('/'); const docParts = docDir.split('/'); let i = 0; while (i < includeParts.length && i < docParts.length && includeParts[i] === docParts[i]) i++; const baseRoot = includeParts.slice(0, i).join('/') || '/'; let newVal; if (original.startsWith('./') || original.startsWith('../')) { // explicit relative paths should resolve relative to the include file const resolved = new URL(original, includeUrl); newVal = resolved.pathname + resolved.search + resolved.hash; } else { // treat plain paths (e.g. "assets/..." or "pages/...") as site-root relative newVal = (baseRoot.endsWith('/') ? baseRoot : baseRoot + '/') + original; } node.setAttribute(attr, newVal); }); }); el.replaceWith(fragment); } catch (err) { console.error('Failed to load include', url, err); } })); if (typeof ensureFeather === 'function') { ensureFeather().then(() => { try { if (window.feather && feather.replace) feather.replace(); } catch (e) {} }).catch(()=>{}); } else if (window.feather && feather.replace) { try { feather.replace(); } catch (e) {} } } // wait for includes to be loaded so subsequent initialization can bind to elements inside them await loadIncludes(); const html = document.documentElement; function getPreferredTheme() { const savedTheme = localStorage.getItem('theme'); if (savedTheme) return savedTheme; return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } // Apply preferred theme try { html.setAttribute('data-theme', getPreferredTheme()); } catch (e) {} // Theme toggle (robust) const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) { // sync aria state themeToggle.setAttribute('aria-pressed', html.getAttribute('data-theme') === 'dark' ? 'false' : 'true'); themeToggle.addEventListener('click', () => { const currentTheme = html.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); themeToggle.setAttribute('aria-pressed', newTheme === 'dark' ? 'false' : 'true'); }); } // Navbar scroll effect const navbar = document.getElementById('navbar'); function updateNavbarOnScroll() { if (!navbar) return; navbar.classList.toggle('scrolled', window.scrollY > 50); } window.addEventListener('scroll', updateNavbarOnScroll); window.addEventListener('load', updateNavbarOnScroll); // Mobile menu const mobileToggle = document.getElementById('mobile-toggle'); const navLinks = document.getElementById('nav-links'); // mobile menu initialization (debug logs removed) // create a toggle function that is safe even if elements are added later const toggleMenu = () => { const mt = document.getElementById('mobile-toggle'); const nl = document.getElementById('nav-links'); // console.log('[DEBUG] toggleMenu called', { mt: !!mt, nl: !!nl, mtClass: mt ? mt.className : null, nlClass: nl ? nl.className : null }); if (!mt || !nl) { // elements missing: abort toggle // console.log('[DEBUG] toggleMenu aborted: missing elements', { mt, nl }); return; } mt.classList.toggle('active'); nl.classList.toggle('active'); // console.log('[DEBUG] toggleMenu toggled', { mtClass: mt.className, nlClass: nl.className }); const expanded = nl.classList.contains('active') ? 'true' : 'false'; mt.setAttribute('aria-expanded', expanded); }; // |||||||||||||| ça fait buguer le menu mobile |||||||||||||| // direct binding if elements exist // if (mobileToggle && navLinks) { // if (!mobileToggle.hasAttribute('tabindex')) mobileToggle.setAttribute('tabindex', '0'); // mobileToggle.setAttribute('aria-expanded', navLinks.classList.contains('active') ? 'true' : 'false'); // mobileToggle.addEventListener('click', (e) => { // console.log('[DEBUG] mobileToggle click handler fired', e.target, e); // e.preventDefault(); // toggleMenu(); // }); // mobileToggle.addEventListener('keydown', (e) => { // if (e.key === 'Enter' || e.key === ' ') { // console.log('[DEBUG] mobileToggle keydown:', e.key); // e.preventDefault(); // toggleMenu(); // } // }); // navLinks.querySelectorAll('a').forEach(link => { // link.addEventListener('click', () => { // console.log('[DEBUG] nav link clicked in mobile menu', link.getAttribute('href')); // // collapse on link click // const mt = document.getElementById('mobile-toggle'); // const nl = document.getElementById('nav-links'); // if (mt && nl) { // mt.classList.remove('active'); // nl.classList.remove('active'); // mt.setAttribute('aria-expanded', 'false'); // } // }); // }); // } // Global delegation: catch clicks anywhere on page for '#mobile-toggle' (works even if element is replaced) document.addEventListener('click', (e) => { const mt = e.target.closest ? e.target.closest('#mobile-toggle') : null; if (mt) { // console.log('[DEBUG] delegated click caught for #mobile-toggle', e.target); e.preventDefault(); toggleMenu(); } }); // expose helper for console debugging window.toggleMobileMenu = toggleMenu; // Smooth scroll for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { const href = this.getAttribute('href'); if (!href || href === '#') return; const target = document.querySelector(href); if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Highlight active nav link based on current URL function normalizePath(path) { if (!path) return '/'; // treat both '/' and '/index.html' as root if (path.endsWith('index.html')) { const p = path.slice(0, -'index.html'.length); return p === '' ? '/' : p; } return path.endsWith('/') ? path : path; } function setActiveNavLink() { const links = document.querySelectorAll('#nav-links a[href]'); if (!links.length) return; const currentPath = normalizePath(window.location.pathname); const currentHash = window.location.hash || ''; let bestMatch = null; let bestScore = -1; links.forEach(link => { const href = link.getAttribute('href'); let linkUrl; try { linkUrl = new URL(href, document.location.href); } catch (e) { return; } const linkPath = normalizePath(linkUrl.pathname); const linkHash = linkUrl.hash || ''; let score = 0; if (linkPath === currentPath) score += 2; if (linkHash && linkHash === currentHash) score += 4; // Prefer exact hash matches, then exact path matches if (score > bestScore) { bestScore = score; bestMatch = link; } }); // Fallback: if on root (/) and there is an index link, pick it if (!bestMatch) { links.forEach(link => { if (!bestMatch && /index\.html$|\/$/.test(link.getAttribute('href') || '')) { bestMatch = link; } }); } links.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); if (bestMatch) { bestMatch.classList.add('active'); bestMatch.setAttribute('aria-current', 'page'); } } // initialize and listen for navigation/hash changes setActiveNavLink(); window.addEventListener('hashchange', setActiveNavLink); window.addEventListener('popstate', setActiveNavLink); // Home page: observe sections so #apropos and #contact become active when visible, // and revert to "Accueil" when neither section is in view. let homeSectionObserver = null; let homeVisibility = {}; function initHomeSectionObserver() { // clean existing observer if (homeSectionObserver) { homeSectionObserver.disconnect(); homeSectionObserver = null; homeVisibility = {}; } // Instead of requiring a strict '/' path, detect whether the current document // actually contains any sections referenced by nav links (e.g. #apropos, #contact). const links = Array.from(document.querySelectorAll('#nav-links a[href]')); const sectionLinks = links.filter(link => { try { const u = new URL(link.getAttribute('href'), document.location.href); if (!u.hash) return false; return !!document.querySelector(u.hash); } catch (e) { return false; } }); if (!sectionLinks.length) return; const sections = sectionLinks.map(link => { const hash = new URL(link.getAttribute('href'), document.location.href).hash; const el = document.querySelector(hash); return el ? { id: el.id, el, link } : null; }).filter(Boolean); if (!sections.length) return; // setup observer homeSectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { homeVisibility[entry.target.id] = entry.intersectionRatio || 0; }); // pick the section with highest intersection ratio let bestId = null; let bestRatio = 0; Object.entries(homeVisibility).forEach(([id, ratio]) => { if (ratio > bestRatio) { bestRatio = ratio; bestId = id; } }); const navLinks = Array.from(document.querySelectorAll('#nav-links a[href]')); // if a section is sufficiently visible, activate its link if (bestId && bestRatio > 0.15) { const targetLink = navLinks.find(l => { try { return new URL(l.getAttribute('href'), document.location.href).hash === '#' + bestId; } catch (e) { return false; } }); if (targetLink) { navLinks.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); targetLink.classList.add('active'); targetLink.setAttribute('aria-current', 'page'); return; } } // fallback: set Accueil active when no section is visible const indexLink = Array.from(navLinks).find(l => /index\.html$|\/$/.test(l.getAttribute('href') || '')); navLinks.forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); if (indexLink) { indexLink.classList.add('active'); indexLink.setAttribute('aria-current', 'page'); } }, { root: null, threshold: [0, 0.15, 0.3, 0.6] }); sections.forEach(s => { homeVisibility[s.id] = 0; homeSectionObserver.observe(s.el); }); } // initialize observer on load and update when navigation changes initHomeSectionObserver(); window.addEventListener('resize', () => { if (homeSectionObserver) initHomeSectionObserver(); }); // re-init observer when URL path changes (i.e., leave/enter index) const originalSetActive = setActiveNavLink; function wrappedSetActive() { // if leaving index, disconnect observer const path = normalizePath(window.location.pathname); if (path !== '/') { if (homeSectionObserver) { homeSectionObserver.disconnect(); homeSectionObserver = null; homeVisibility = {}; } originalSetActive(); return; } // on index, ensure observer is active and let it control the highlight where possible initHomeSectionObserver(); originalSetActive(); } // replace handlers so navigation updates use our wrapped variant window.removeEventListener('hashchange', setActiveNavLink); window.removeEventListener('popstate', setActiveNavLink); window.addEventListener('hashchange', wrappedSetActive); window.addEventListener('popstate', wrappedSetActive); // Reveal on scroll const revealElements = document.querySelectorAll('.reveal'); function revealOnScroll() { if (!revealElements.length) return; revealElements.forEach(el => { const elementTop = el.getBoundingClientRect().top; if (elementTop < window.innerHeight - 100) { el.classList.add('active'); } }); } window.addEventListener('scroll', revealOnScroll); window.addEventListener('load', revealOnScroll); // Contact form (if exists) const contactForm = document.getElementById('contact-form'); if (contactForm) { contactForm.addEventListener('submit', function (e) { e.preventDefault(); alert('Merci pour votre message ! Nous vous répondrons très bientôt.'); this.reset(); }); } });