diff --git a/wp-content/themes/apus-theme/assets/css/toc.css b/wp-content/themes/apus-theme/assets/css/toc.css index 480732a4..596e3715 100644 --- a/wp-content/themes/apus-theme/assets/css/toc.css +++ b/wp-content/themes/apus-theme/assets/css/toc.css @@ -19,7 +19,9 @@ padding: 1.5rem; margin: 2rem 0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - position: relative; + position: sticky; + top: 5.5rem; + z-index: 10; } .apus-toc-header { @@ -170,11 +172,13 @@ .apus-toc-link { color: #212529; text-decoration: none; - display: inline-block; - transition: color 0.2s ease, transform 0.2s ease; + display: block; + transition: color 0.2s ease, transform 0.2s ease, border-color 0.2s ease; line-height: 1.5; position: relative; - padding: 0.25rem 0; + padding: 0.25rem 0 0.25rem 1rem; + border-left: 3px solid transparent; + margin-left: -1rem; } .apus-toc-link:hover { @@ -188,22 +192,11 @@ border-radius: 2px; } -/* Active link highlighting */ +/* Active link highlighting - Issue #55 */ .apus-toc-link.active { color: #0d6efd; font-weight: 600; -} - -.apus-toc-link.active::after { - content: ''; - position: absolute; - left: -1rem; - top: 50%; - transform: translateY(-50%); - width: 4px; - height: 100%; - background-color: #0d6efd; - border-radius: 2px; + border-left-color: #0d6efd; } /* ======================================== @@ -348,16 +341,16 @@ h3[id] { } .apus-toc-list::-webkit-scrollbar-thumb { - background: #888; + background: #cbd5e0; border-radius: 3px; } .apus-toc-list::-webkit-scrollbar-thumb:hover { - background: #555; + background: #a0aec0; } /* Firefox */ .apus-toc-list { scrollbar-width: thin; - scrollbar-color: #888 #f1f1f1; + scrollbar-color: #cbd5e0 #f1f1f1; } diff --git a/wp-content/themes/apus-theme/assets/js/toc.js b/wp-content/themes/apus-theme/assets/js/toc.js index 20586107..4426e9c8 100644 --- a/wp-content/themes/apus-theme/assets/js/toc.js +++ b/wp-content/themes/apus-theme/assets/js/toc.js @@ -94,7 +94,7 @@ } // Update active state - updateActiveLink(this); + updateActiveLinkOnClick(this); // Focus the target heading for accessibility targetElement.setAttribute('tabindex', '-1'); @@ -104,7 +104,8 @@ } /** - * Initialize active link highlighting based on scroll position + * Initialize active link highlighting with IntersectionObserver + * Issue #55 - ScrollSpy implementation */ function initActiveHighlight() { const tocLinks = document.querySelectorAll('.apus-toc-link'); @@ -114,51 +115,67 @@ return; } - let ticking = false; + // Keep track of which headings are currently visible + const visibleHeadings = new Set(); - // Debounced scroll handler - function onScroll() { - if (!ticking) { - window.requestAnimationFrame(function() { - updateActiveOnScroll(headings, tocLinks); - ticking = false; - }); - ticking = true; - } - } + // IntersectionObserver configuration with custom rootMargin + // -20% top, -35% bottom for optimal detection + const observerOptions = { + root: null, // viewport + rootMargin: '-20% 0px -35% 0px', + threshold: 0 + }; - window.addEventListener('scroll', onScroll, { passive: true }); + // Callback when heading visibility changes + const observerCallback = function(entries) { + entries.forEach(function(entry) { + const headingId = entry.target.id; - // Initial update - updateActiveOnScroll(headings, tocLinks); + 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 scroll position + * Update active link based on visible headings * - * @param {Array} headings Array of heading elements + * @param {Set} visibleHeadings Set of currently visible heading IDs * @param {NodeList} tocLinks TOC link elements + * @param {Array} headings Array of all heading elements */ - function updateActiveOnScroll(headings, tocLinks) { - const scrollPosition = window.scrollY + 100; // Offset for better UX + function updateActiveLink(visibleHeadings, tocLinks, headings) { + // If no headings are visible, don't change anything + if (visibleHeadings.size === 0) { + return; + } - // Find the current heading - let currentHeading = null; - for (let i = headings.length - 1; i >= 0; i--) { - if (headings[i].offsetTop <= scrollPosition) { - currentHeading = headings[i]; + // 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; } } - // If we're at the top, use the first heading - if (!currentHeading && scrollPosition < headings[0].offsetTop) { - currentHeading = headings[0]; - } - - // Update active class + // Update active class on TOC links tocLinks.forEach(function(link) { - if (currentHeading && link.getAttribute('href') === '#' + currentHeading.id) { + if (activeHeading && link.getAttribute('href') === '#' + activeHeading.id) { link.classList.add('active'); } else { link.classList.remove('active'); @@ -171,7 +188,7 @@ * * @param {Element} clickedLink The clicked TOC link */ - function updateActiveLink(clickedLink) { + function updateActiveLinkOnClick(clickedLink) { const tocLinks = document.querySelectorAll('.apus-toc-link'); tocLinks.forEach(function(link) { link.classList.remove('active'); @@ -198,7 +215,7 @@ behavior: 'smooth', block: 'start' }); - updateActiveLink(tocLink); + updateActiveLinkOnClick(tocLink); }, 100); } }