carhop__dynamiques-theme__P.../resources/js/singles/sommaire.ts
Nonimart 53e280383e
All checks were successful
continuous-integration/drone/push Build is passing
FEATURE + REFACTOR Handling sommaire chapter progression + refactoring footnote hebaviour
2025-09-25 11:59:29 +02:00

229 lines
7.6 KiB
TypeScript

/* ********************************************************
* ************* 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<Element> = 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<Element> = 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);
});
}
}