/** * Table of Contents JavaScript * * Provides smooth scrolling and active link highlighting for the TOC. * Pure vanilla JavaScript - no jQuery dependency. * * @package Apus_Theme * @since 1.0.0 */ (function() { 'use strict'; /** * Initialize TOC functionality when DOM is ready */ function initTOC() { const toc = document.querySelector('.apus-toc'); if (!toc) { return; // No TOC on this page } initToggleButton(); initSmoothScroll(); initActiveHighlight(); } /** * Initialize toggle button functionality */ function initToggleButton() { const toggleButton = document.querySelector('.apus-toc-toggle'); const tocList = document.querySelector('.apus-toc-list'); if (!toggleButton || !tocList) { return; } toggleButton.addEventListener('click', function() { const isExpanded = this.getAttribute('aria-expanded') === 'true'; this.setAttribute('aria-expanded', !isExpanded); // Save state to localStorage try { localStorage.setItem('apus-toc-collapsed', isExpanded ? 'true' : 'false'); } catch (e) { // localStorage might not be available } }); // Restore saved state try { const isCollapsed = localStorage.getItem('apus-toc-collapsed') === 'true'; if (isCollapsed) { toggleButton.setAttribute('aria-expanded', 'false'); } } catch (e) { // localStorage might not be available } } /** * Initialize smooth scrolling for TOC links * Respeta preferencia de movimiento reducido */ function initSmoothScroll() { // Verificar si el usuario prefiere movimiento reducido const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const tocLinks = document.querySelectorAll('.apus-toc-link'); tocLinks.forEach(function(link) { link.addEventListener('click', function(e) { e.preventDefault(); const targetId = this.getAttribute('href').substring(1); const targetElement = document.getElementById(targetId); if (!targetElement) { return; } // Smooth scroll to target (o auto si prefiere movimiento reducido) targetElement.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth', block: 'start' }); // Update URL without jumping if (history.pushState) { history.pushState(null, null, '#' + targetId); } else { window.location.hash = targetId; } // Update active state updateActiveLinkOnClick(this); // Focus the target heading for accessibility targetElement.setAttribute('tabindex', '-1'); targetElement.focus(); }); }); } /** * Initialize active link highlighting with IntersectionObserver * Issue #55 - ScrollSpy implementation */ function initActiveHighlight() { const tocLinks = document.querySelectorAll('.apus-toc-link'); const headings = Array.from(document.querySelectorAll('h2[id], h3[id]')); if (headings.length === 0) { return; } // Keep track of which headings are currently visible const visibleHeadings = new Set(); // IntersectionObserver configuration with custom rootMargin // -20% top, -35% bottom for optimal detection const observerOptions = { root: null, // viewport rootMargin: '-20% 0px -35% 0px', threshold: 0 }; // Callback when heading visibility changes const observerCallback = function(entries) { entries.forEach(function(entry) { const headingId = entry.target.id; if (entry.isIntersecting) { visibleHeadings.add(headingId); } else { visibleHeadings.delete(headingId); } }); // Update active link based on visible headings updateActiveLink(visibleHeadings, tocLinks, headings); }; // Create observer const observer = new IntersectionObserver(observerCallback, observerOptions); // Observe all headings headings.forEach(function(heading) { observer.observe(heading); }); } /** * Update active link based on visible headings * * @param {Set} visibleHeadings Set of currently visible heading IDs * @param {NodeList} tocLinks TOC link elements * @param {Array} headings Array of all heading elements */ function updateActiveLink(visibleHeadings, tocLinks, headings) { // If no headings are visible, don't change anything if (visibleHeadings.size === 0) { return; } // Find the first visible heading (topmost in document order) let activeHeading = null; for (let i = 0; i < headings.length; i++) { if (visibleHeadings.has(headings[i].id)) { activeHeading = headings[i]; break; } } // Update active class on TOC links tocLinks.forEach(function(link) { if (activeHeading && link.getAttribute('href') === '#' + activeHeading.id) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } /** * Update active link when clicked * * @param {Element} clickedLink The clicked TOC link */ function updateActiveLinkOnClick(clickedLink) { const tocLinks = document.querySelectorAll('.apus-toc-link'); tocLinks.forEach(function(link) { link.classList.remove('active'); }); clickedLink.classList.add('active'); } /** * Handle hash navigation on page load */ function handleHashOnLoad() { if (!window.location.hash) { return; } const targetId = window.location.hash.substring(1); const targetElement = document.getElementById(targetId); const tocLink = document.querySelector('.apus-toc-link[href="#' + targetId + '"]'); if (targetElement && tocLink) { // Small delay to ensure page is fully loaded setTimeout(function() { targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); updateActiveLinkOnClick(tocLink); }, 100); } } /** * Initialize when DOM is ready */ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { initTOC(); handleHashOnLoad(); }); } else { // DOM is already ready initTOC(); handleHashOnLoad(); } })();