FEATURE + REFACTOR Handling sommaire chapter progression + refactoring footnote hebaviour
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
36bad45e67
commit
53e280383e
|
|
@ -1,9 +1,84 @@
|
|||
import {
|
||||
scrollToFootnoteInIndexPanel,
|
||||
toggleActiveTabPanel,
|
||||
toggleActiveFootnoteLinkInIndexPanel,
|
||||
} from './index-panel';
|
||||
import { toggleActiveTabPanel } from './index-panel';
|
||||
|
||||
/* ********************************************************
|
||||
* ************* SCROLLING FUNCTIONS *********************
|
||||
* ********************************************************
|
||||
*/
|
||||
|
||||
export function scrollToFootnoteInIndexPanel(footnoteId: string): void {
|
||||
const footnotesIndex = document.querySelector('#footnotes-index') as HTMLElement;
|
||||
const footnotesIndexContainer = document.querySelector('ul#footnotes-index');
|
||||
const indexPanelContent = document.querySelector('.index-panel__content') as HTMLElement;
|
||||
const currentFootnote = footnotesIndexContainer?.querySelector(
|
||||
`a[href="#${footnoteId}"]`
|
||||
) as HTMLElement;
|
||||
|
||||
if (currentFootnote && footnotesIndexContainer) {
|
||||
const containerRect = footnotesIndexContainer.getBoundingClientRect();
|
||||
const elementRect = currentFootnote.getBoundingClientRect();
|
||||
|
||||
const relativeTop = elementRect.top - containerRect.top;
|
||||
const scrollTop = footnotesIndexContainer.scrollTop + relativeTop - 20;
|
||||
|
||||
footnotesIndexContainer.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollToFootnote(footnoteId: string): void {
|
||||
const footnote = document.querySelector(`a.footnote-reference#${footnoteId}`);
|
||||
if (!footnote) return;
|
||||
|
||||
footnote.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
/* ********************************************************
|
||||
* ************* TOGGLE ACTIVE ***************************
|
||||
* ********************************************************
|
||||
*/
|
||||
export function toggleActiveFootnoteLinkInIndexPanel(footnoteId: string): void {
|
||||
const footnotesIndexLinks = document.querySelectorAll('.footnote-reference-item');
|
||||
const indexPanel = document.querySelector('.index-panel') as HTMLElement;
|
||||
const currentFootnote = indexPanel.querySelector(`a[href="#${footnoteId}"]`) as HTMLElement;
|
||||
|
||||
footnotesIndexLinks.forEach((footnoteLink) => {
|
||||
footnoteLink.setAttribute('active', 'false');
|
||||
});
|
||||
|
||||
currentFootnote?.setAttribute('active', 'true');
|
||||
}
|
||||
|
||||
/* ********************************************************
|
||||
* ************* OBSERVE LINKS ***************************
|
||||
* ********************************************************
|
||||
*/
|
||||
export function observeFootnotesLinks(): void {
|
||||
const footnotesLinks = document.querySelectorAll('.footnotes-index a');
|
||||
|
||||
footnotesLinks.forEach((footnoteLink) => {
|
||||
footnoteLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = e.target as HTMLElement;
|
||||
const href = target.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
const targetId = href.startsWith('#') ? href.substring(1) : href;
|
||||
if (!targetId) return;
|
||||
scrollToFootnote(targetId);
|
||||
// document.querySelector(`#${targetId}`)?.scrollIntoView({ behavior: 'smooth' });
|
||||
|
||||
toggleActiveFootnoteLinkInIndexPanel(targetId);
|
||||
scrollToFootnoteInIndexPanel(targetId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ********************************************************
|
||||
* ************* MAIN — HANDLE GENERAL BEHAVIOUR *********
|
||||
* ********************************************************
|
||||
*/
|
||||
export default function handleFootnoteFormat(): void {
|
||||
const footnotes = document.querySelectorAll('.content-area .footnote-reference');
|
||||
|
||||
|
|
@ -18,10 +93,3 @@ export default function handleFootnoteFormat(): void {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function scrollToFootnote(footnoteId: string): void {
|
||||
const footnote = document.querySelector(`a.footnote-reference#${footnoteId}`);
|
||||
if (!footnote) return;
|
||||
|
||||
footnote.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import { scrollToFootnote } from './footnote-format';
|
||||
import { handleSmoothScrollToTitle } from './sommaire';
|
||||
import handleChapterProgression, { observeSommaireLinks } from './sommaire';
|
||||
import { observeFootnotesLinks } from './footnote-format';
|
||||
|
||||
export default function handleIndexPanel(): void {
|
||||
const indexPanel = document.querySelector('.index-panel');
|
||||
if (!indexPanel) return;
|
||||
|
||||
// TABS
|
||||
observeTabsButtons();
|
||||
// FOOTNOTES
|
||||
observeFootnotesLinks();
|
||||
// INDEX PANEL MOBILE
|
||||
handleMobileOpenToggle();
|
||||
// CHAPITRES
|
||||
observeSommaireLinks();
|
||||
handleChapterProgression();
|
||||
handleMobileOpenToggle();
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +25,7 @@ function handleMobileOpenToggle(): void {
|
|||
|
||||
mobileOpenToggle.addEventListener('click', () => {
|
||||
const isMobileOpen = indexPanel.getAttribute('data-mobile-open');
|
||||
console.log(isMobileOpen);
|
||||
|
||||
if (isMobileOpen === 'true') {
|
||||
indexPanel.setAttribute('data-mobile-open', 'false');
|
||||
} else {
|
||||
|
|
@ -63,99 +69,3 @@ export function toggleActiveTabPanel(dataIndex: string): void {
|
|||
activeButton.setAttribute('aria-selected', 'true');
|
||||
activePanel.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
// ********************************************************
|
||||
// ************* SOMMAIRE *********************************
|
||||
// ********************************************************
|
||||
|
||||
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;
|
||||
|
||||
handleSmoothScrollToTitle(targetId);
|
||||
toggleActiveChapterLinkInIndexPanel(targetId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// ********************************************************
|
||||
// ************* FOOTNOTES *********************************
|
||||
// ********************************************************
|
||||
|
||||
function observeFootnotesLinks(): void {
|
||||
const footnotesLinks = document.querySelectorAll('.footnotes-index a');
|
||||
|
||||
footnotesLinks.forEach((footnoteLink) => {
|
||||
footnoteLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = e.target as HTMLElement;
|
||||
const href = target.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
const targetId = href.startsWith('#') ? href.substring(1) : href;
|
||||
if (!targetId) return;
|
||||
scrollToFootnote(targetId);
|
||||
// document.querySelector(`#${targetId}`)?.scrollIntoView({ behavior: 'smooth' });
|
||||
|
||||
toggleActiveFootnoteLinkInIndexPanel(targetId);
|
||||
scrollToFootnoteInIndexPanel(targetId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleActiveFootnoteLinkInIndexPanel(footnoteId: string): void {
|
||||
const footnotesIndexLinks = document.querySelectorAll('.footnote-reference-item');
|
||||
const indexPanel = document.querySelector('.index-panel') as HTMLElement;
|
||||
const currentFootnote = indexPanel.querySelector(`a[href="#${footnoteId}"]`) as HTMLElement;
|
||||
|
||||
footnotesIndexLinks.forEach((footnoteLink) => {
|
||||
footnoteLink.setAttribute('active', 'false');
|
||||
});
|
||||
|
||||
currentFootnote?.setAttribute('active', 'true');
|
||||
}
|
||||
|
||||
export function scrollToFootnoteInIndexPanel(footnoteId: string): void {
|
||||
const footnotesIndex = document.querySelector('#footnotes-index') as HTMLElement;
|
||||
const footnotesIndexContainer = document.querySelector('ul#footnotes-index');
|
||||
const indexPanelContent = document.querySelector('.index-panel__content') as HTMLElement;
|
||||
const currentFootnote = footnotesIndexContainer?.querySelector(
|
||||
`a[href="#${footnoteId}"]`
|
||||
) as HTMLElement;
|
||||
|
||||
if (currentFootnote && footnotesIndexContainer) {
|
||||
const containerRect = footnotesIndexContainer.getBoundingClientRect();
|
||||
const elementRect = currentFootnote.getBoundingClientRect();
|
||||
|
||||
const relativeTop = elementRect.top - containerRect.top;
|
||||
const scrollTop = footnotesIndexContainer.scrollTop + relativeTop - 20;
|
||||
|
||||
footnotesIndexContainer.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import handleFootnoteFormat from './footnote-format';
|
|||
import handleCiteButton from './cite-button';
|
||||
import handleLikeButton from './like-button.ts';
|
||||
import handleShareButton from './share-button.ts';
|
||||
|
||||
import { handleArticleToolbar } from './article-toolbar.ts';
|
||||
import { handleRevueToolbar } from './revue-toolbar.ts';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,228 @@
|
|||
/* ********************************************************
|
||||
* ************* 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; // 10px offset from top
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user