Site personnel courcellematthi.eu
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* scroll-spy.js — Mise en évidence de la section courante
|
||||
* =============================================================================
|
||||
* Marque le lien de navigation correspondant à la section actuellement lue.
|
||||
*
|
||||
* Choix technique : IntersectionObserver plutôt qu'un écouteur « scroll ».
|
||||
* Un écouteur de scroll se déclenche des dizaines de fois par seconde et
|
||||
* impose de recalculer getBoundingClientRect() à chaque appel, ce qui force
|
||||
* le navigateur à recalculer la mise en page (layout thrashing) et fait
|
||||
* chuter la fluidité. IntersectionObserver délègue ce travail au moteur de
|
||||
* rendu, hors du fil principal, et ne notifie qu'aux franchissements de seuil.
|
||||
*
|
||||
* Le lien actif reçoit aria-current="true" : ce n'est pas qu'un crochet de
|
||||
* style, c'est l'annonce de la position aux lecteurs d'écran (WCAG 2.4.8).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initialise la surveillance des sections.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {NodeListOf<HTMLElement>} options.sections - Sections observées.
|
||||
* @param {NodeListOf<HTMLAnchorElement>} options.links - Liens de navigation.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function initScrollSpy({ sections, links }) {
|
||||
if (!sections?.length || !links?.length) return;
|
||||
|
||||
// Dégradation propre sur navigateur ancien : pas de surbrillance,
|
||||
// mais la navigation reste parfaitement utilisable.
|
||||
if (!("IntersectionObserver" in window)) return;
|
||||
|
||||
/* --- Index des liens par identifiant de section ciblée --------------- */
|
||||
const linksByTargetId = new Map();
|
||||
|
||||
links.forEach((link) => {
|
||||
const targetId = link.getAttribute("href")?.replace("#", "");
|
||||
if (targetId) linksByTargetId.set(targetId, link);
|
||||
});
|
||||
|
||||
/**
|
||||
* Active un lien et désactive tous les autres.
|
||||
* @param {string} sectionId
|
||||
* @returns {void}
|
||||
*/
|
||||
const setActiveLink = (sectionId) => {
|
||||
links.forEach((link) => link.removeAttribute("aria-current"));
|
||||
linksByTargetId.get(sectionId)?.setAttribute("aria-current", "true");
|
||||
};
|
||||
|
||||
/** Identifiants des sections actuellement dans la zone de détection. */
|
||||
const visibleSectionIds = new Set();
|
||||
|
||||
/** Sections dans l'ordre du document, pour départager les ex æquo. */
|
||||
const orderedSections = [...sections];
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(({ target, isIntersecting }) => {
|
||||
if (isIntersecting) visibleSectionIds.add(target.id);
|
||||
else visibleSectionIds.delete(target.id);
|
||||
});
|
||||
|
||||
// Aucune section dans la bande : on conserve le dernier état actif
|
||||
// plutôt que de tout éteindre, ce qui produirait un clignotement.
|
||||
if (visibleSectionIds.size === 0) return;
|
||||
|
||||
// Plusieurs sections visibles : on retient la première dans l'ordre
|
||||
// du document, c'est-à-dire la plus haute à l'écran.
|
||||
const current = orderedSections.find((section) =>
|
||||
visibleSectionIds.has(section.id)
|
||||
);
|
||||
|
||||
if (current) setActiveLink(current.id);
|
||||
},
|
||||
{
|
||||
/*
|
||||
* Bande de détection étroite, positionnée dans le tiers supérieur de
|
||||
* la fenêtre, juste sous l'en-tête collant.
|
||||
*
|
||||
* -20% en haut : ignore ce qui passe derrière l'en-tête
|
||||
* -70% en bas : ignore ce qui n'est encore qu'en bas d'écran
|
||||
*
|
||||
* Résultat : la section considérée « courante » est celle que
|
||||
* l'utilisateur lit réellement, pas celle qui pointe le nez.
|
||||
*/
|
||||
rootMargin: "-20% 0px -70% 0px",
|
||||
threshold: 0,
|
||||
}
|
||||
);
|
||||
|
||||
sections.forEach((section) => observer.observe(section));
|
||||
}
|
||||
Reference in New Issue
Block a user