/* ******************************************************** * ************* SCROLLING FUNCTIONS ********************* * ******************************************************** */ /** * Effectue un scroll fluide vers un élément ciblé avec un offset personnalisé. * @param targetId - ID de l'élément cible (sans le #) */ export function handleSmoothScrollToTitle(targetId: string): void { const targetElement = document.querySelector(`#${targetId}`); if (!targetElement) return; const elementRect = targetElement.getBoundingClientRect(); const offset = 30; // 30px offset from top window.scrollTo({ top: window.pageYOffset + elementRect.top - offset, behavior: 'smooth', }); } /** * Fait défiler l'indicateur de position vers le chapitre actif dans l'index panel. * @param targetId - ID du chapitre cible */ export function scrollToActiveChapterInIndexPanel(targetId: string): void { // const activeLink = document.querySelector(`a[active="true"]`) as HTMLElement; // const targetPosition = activeLink.offsetTop; // const targetHeight = activeLink.offsetHeight; // let chapterIndicator = document.querySelector('.chapter_index__position-indicator'); // chapterIndicator.style.top = targetPosition + 'px'; // chapterIndicator.style.height = targetHeight + 'px'; } // ******************************************************** // ************* TOGGLE ACTIVE ************************** // ******************************************************** /** * Désactive tous les liens de chapitre précédemment actifs. */ function removePreviousActiveLink() { const activeLinks = document.querySelectorAll( '.index-panel__content .sommaire-index li a[active="true"]' ); activeLinks.forEach((link) => { link.setAttribute('active', 'false'); }); } /** * Active le lien de chapitre correspondant à l'ID cible dans l'index panel. * @param targetId - ID du chapitre à activer */ export function toggleActiveChapterLinkInIndexPanel(targetId: string): void { const sommaireLinks: NodeListOf = document.querySelectorAll('.sommaire-index li a'); const indexPanel = document.querySelector('.index-panel') as HTMLElement; const currentLink = indexPanel.querySelector(`a[href="#${targetId}"]`) as HTMLElement; if (!currentLink) return; for (const link of sommaireLinks) { link.setAttribute('active', 'false'); } currentLink?.setAttribute('active', 'true'); } // ******************************************************** // ************* CHAPTER OBSERVER STATE MANAGEMENT ******* // ******************************************************** // Variable globale pour contrôler l'état de pause de l'intersection observer let isChapterObserverPaused = false; /** * Met en pause ou réactive l'intersection observer des chapitres. * @param paused - État de pause à définir */ export function setChapterObserverPaused(paused: boolean): void { isChapterObserverPaused = paused; } /** * Retourne l'état actuel de pause de l'intersection observer. * @returns État de pause de l'observer */ export function getChapterObserverPausedState(): boolean { return isChapterObserverPaused; } /** * Ajoute des écouteurs d'événements aux liens de chapitre pour la navigation. */ function observeChapterLinks(): void { let chapterLinks = document.querySelectorAll('.chapter_index__link'); if (!chapterLinks) return; chapterLinks.forEach((link) => { link.addEventListener('click', (e) => { handleLinkScrollToTarget(e); }); }); } /** * Intersection Observer qui détecte automatiquement les chapitres visibles * et met à jour l'état actif dans l'index panel. */ const chapterProgressionObserver = new IntersectionObserver( (entries) => { // Ne pas traiter les entrées si l'observer est en pause (pendant un clic) const isIntersetionObserverPaused = getChapterObserverPausedState(); if (isIntersetionObserverPaused) return; entries.forEach((entry) => { const blockId = entry.target.getAttribute('id'); const relatedChapterLink = document.querySelector(`a[href="#${blockId}"]`); if (entry.isIntersecting) { removePreviousActiveLink(); entry.target.setAttribute('active', 'true'); relatedChapterLink?.setAttribute('active', 'true'); } }); }, { rootMargin: '-10% 0px -50% 0px', } ); /** * Gère le scroll vers un élément cible lors du clic sur un lien. * @param e - Événement du clic */ function handleLinkScrollToTarget(e: Event) { e.preventDefault(); let target = (e.target as HTMLElement).getAttribute('href'); if (!target) return; let targetBlock = document.querySelector(target); if (!targetBlock) return; targetBlock.setAttribute('tabindex', '-1'); targetBlock.scrollIntoView({ behavior: 'smooth', }); (targetBlock as HTMLElement).focus({ preventScroll: true }); } // ******************************************************** // ************* MAIN FUNCTION **************************** // ******************************************************** /** * Initialise le système de progression des chapitres avec l'intersection observer. * Active la détection automatique des chapitres visibles lors du scroll. */ export default function handleChapterProgression() { const hasChapterIndex = document.querySelector('.index-panel__content .sommaire-index'); if (!hasChapterIndex) return; // Debug function to visualize rootMargin // function createDebugOverlay() { // const overlay = document.createElement('div'); // overlay.style.position = 'fixed'; // overlay.style.top = '10%'; // overlay.style.bottom = '50%'; // overlay.style.left = '0'; // overlay.style.right = '0'; // overlay.style.border = '2px solid red'; // overlay.style.pointerEvents = 'none'; // overlay.style.zIndex = '9999'; // overlay.style.opacity = '0.3'; // overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)'; // document.body.appendChild(overlay); // } // Uncomment the line below to see the detection zone // createDebugOverlay(); // Initialiser les écouteurs de liens observeChapterLinks(); // Observer tous les titres h2 de l'article const titlesBlocks = document.querySelectorAll('.article-content h2'); titlesBlocks.forEach((block) => { chapterProgressionObserver.observe(block); }); } // ******************************************************** // ************* SOMMAIRE NAVIGATION ********************** // ******************************************************** /** * Ajoute des écouteurs d'événements aux liens du sommaire pour la navigation manuelle. * Gère le conflit avec l'intersection observer en le pausant temporairement. */ export function observeSommaireLinks(): void { const sommaireTitles: NodeListOf = document.querySelectorAll('.sommaire-index li a'); for (const title of sommaireTitles) { title.addEventListener('click', (e) => { e.preventDefault(); const href = title.getAttribute('href'); if (!href) return; const targetId = href.startsWith('#') ? href.substring(1) : href; if (!targetId) return; // Désactiver temporairement l'intersection observer pour éviter les conflits setChapterObserverPaused(true); handleSmoothScrollToTitle(targetId); toggleActiveChapterLinkInIndexPanel(targetId); // Réactiver l'intersection observer après le smooth scroll setTimeout(() => { setChapterObserverPaused(false); }, 1000); }); } }