229 lines
7.6 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
}
|