diff --git a/resources/js/singles/footnote-format.ts b/resources/js/singles/footnote-format.ts index 12dab1c..ba8ad49 100644 --- a/resources/js/singles/footnote-format.ts +++ b/resources/js/singles/footnote-format.ts @@ -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' }); -} diff --git a/resources/js/singles/index-panel.ts b/resources/js/singles/index-panel.ts index 10c0b33..713a90d 100644 --- a/resources/js/singles/index-panel.ts +++ b/resources/js/singles/index-panel.ts @@ -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 = 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 = 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', - }); - } -} diff --git a/resources/js/singles/singles.ts b/resources/js/singles/singles.ts index 0cfe213..540ad42 100644 --- a/resources/js/singles/singles.ts +++ b/resources/js/singles/singles.ts @@ -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'; diff --git a/resources/js/singles/sommaire.ts b/resources/js/singles/sommaire.ts index 4de4879..6f498bb 100644 --- a/resources/js/singles/sommaire.ts +++ b/resources/js/singles/sommaire.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 = 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); + }); + } +}