From 2e597f5b8f64b0d55c96abd2fcae00a1b317eca6 Mon Sep 17 00:00:00 2001 From: Jeremy Karst Date: Thu, 3 Apr 2025 16:10:58 -0400 Subject: [PATCH] Added TOC highlighting --- assets/css/custom.css | 46 ++++++++- assets/js/toc-highlight.js | 151 ++++++++++++++++++++++++++++ layouts/partials/extend-footer.html | 4 + layouts/partials/scripts.html | 3 - 4 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 assets/js/toc-highlight.js create mode 100644 layouts/partials/extend-footer.html delete mode 100644 layouts/partials/scripts.html diff --git a/assets/css/custom.css b/assets/css/custom.css index c242704..5a46a09 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -17,9 +17,12 @@ } .toc:has(#TableOfContents) { - background-color: rgba(25, 25, 35, 0.45); + background-color: rgba(25, 25, 35, 0.3); border-radius: 10px; max-height: 70vh; + overflow-y: auto; + padding: 1rem; + scroll-behavior: smooth; } .background-container { @@ -107,4 +110,45 @@ header button[id^="search-button"]:hover { 100% { backdrop-filter: blur(8px) hue-rotate(-10deg); } +} + +/* TOC link styling */ +#TableOfContents a { + color: rgba(255, 255, 255, 0.8); + text-decoration: none; + transition: all 0.3s ease; + display: block; + padding: 0.25rem 0; +} + +#TableOfContents a:hover { + color: white; + transform: translateX(4px); +} + +/* Active TOC item */ +#TableOfContents a.active { + color: white; + font-weight: 600; + transform: translateX(4px); + position: relative; + transition: all 0.3s ease; +} + +#TableOfContents a.active::before { + content: ''; + position: absolute; + left: -1rem; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 1rem; + background: linear-gradient(to bottom, #4a9eff, #2d7bda); + border-radius: 2px; + transition: all 0.3s ease; +} + +/* Remove the default smooth scrolling behavior */ +html { + scroll-behavior: smooth; } \ No newline at end of file diff --git a/assets/js/toc-highlight.js b/assets/js/toc-highlight.js new file mode 100644 index 0000000..c1f680a --- /dev/null +++ b/assets/js/toc-highlight.js @@ -0,0 +1,151 @@ +document.addEventListener('DOMContentLoaded', function() { + const tocLinks = document.querySelectorAll('#TableOfContents a'); + const tocContainer = document.querySelector('.toc'); + const sections = new Map(); // Store section elements with their corresponding TOC links + let lastActiveSection = null; + let scrollTimeout = null; + let isScrolling = false; + + // Build a map of section IDs to their TOC links + tocLinks.forEach(link => { + const sectionId = decodeURIComponent(link.getAttribute('href').substring(1)); + const section = document.getElementById(sectionId); + if (section) { + sections.set(section, link); + } + }); + + // Function to scroll TOC container to keep active item in view + const scrollTocToActive = (activeLink) => { + if (!tocContainer || !activeLink) return; + + const containerRect = tocContainer.getBoundingClientRect(); + const linkRect = activeLink.getBoundingClientRect(); + + // Check if the active link is outside the visible area + if (linkRect.top < containerRect.top || linkRect.bottom > containerRect.bottom) { + // Calculate the scroll position to center the active link + const scrollTop = activeLink.offsetTop - (containerRect.height / 2) + (linkRect.height / 2); + + // Use CSS smooth scrolling if available, otherwise use JS + if ('scrollBehavior' in document.documentElement.style) { + tocContainer.scrollTo({ + top: scrollTop, + behavior: 'smooth' + }); + } else { + // Fallback for browsers that don't support smooth scrolling + tocContainer.scrollTop = scrollTop; + } + } + }; + + // Function to find the section closest to the center of the viewport + const findClosestSectionToCenter = () => { + let closestSection = null; + let closestDistance = Infinity; + const viewportHeight = window.innerHeight; + const viewportCenter = viewportHeight / 2; + + sections.forEach((link, section) => { + const rect = section.getBoundingClientRect(); + // Calculate the center of the section + const sectionCenter = rect.top + (rect.height / 2); + // Calculate distance from the center of the viewport + const distance = Math.abs(sectionCenter - viewportCenter); + + // If this section is closer to the center than our current closest + if (distance < closestDistance) { + closestDistance = distance; + closestSection = section; + } + }); + + return closestSection; + }; + + // Function to update the active section + const updateActiveSection = (activeSection) => { + if (!activeSection) return; + + // Update active state + tocLinks.forEach(link => link.classList.remove('active')); + + const activeLink = sections.get(activeSection); + if (activeLink) { + activeLink.classList.add('active'); + lastActiveSection = activeSection; + + // Use a small delay before scrolling to prevent rapid changes + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + + scrollTimeout = setTimeout(() => { + scrollTocToActive(activeLink); + }, 100); + } + }; + + // Function to handle scroll events + const handleScroll = () => { + // Clear any pending scroll timeout + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + + // Find the section closest to the center of the viewport + const closestSection = findClosestSectionToCenter(); + + // Update the active section + updateActiveSection(closestSection); + }; + + // Add scroll event listener with debouncing + let scrollDebounceTimer = null; + window.addEventListener('scroll', () => { + isScrolling = true; + + // Debounce the scroll event + if (scrollDebounceTimer) { + clearTimeout(scrollDebounceTimer); + } + + scrollDebounceTimer = setTimeout(() => { + isScrolling = false; + handleScroll(); + }, 100); + }); + + // Initial check to ensure a section is highlighted + setTimeout(() => { + handleScroll(); + }, 500); + + // Smooth scroll to section when clicking TOC links + tocLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const targetId = decodeURIComponent(link.getAttribute('href').substring(1)); + const targetSection = document.getElementById(targetId); + if (targetSection) { + // Use our custom smooth scrolling function if available + if (window.smoothScrollTo) { + window.smoothScrollTo(targetSection); + } else { + // Fallback to standard smooth scrolling + targetSection.scrollIntoView({ behavior: 'smooth' }); + } + + // Also update the active section immediately + const activeLink = sections.get(targetSection); + if (activeLink) { + tocLinks.forEach(l => l.classList.remove('active')); + activeLink.classList.add('active'); + lastActiveSection = targetSection; + scrollTocToActive(activeLink); + } + } + }); + }); +}); \ No newline at end of file diff --git a/layouts/partials/extend-footer.html b/layouts/partials/extend-footer.html new file mode 100644 index 0000000..7ee539b --- /dev/null +++ b/layouts/partials/extend-footer.html @@ -0,0 +1,4 @@ +{{ if .TableOfContents }} + {{ $tocHighlight := resources.Get "js/toc-highlight.js" | js.Build | fingerprint }} + +{{ end }} \ No newline at end of file diff --git a/layouts/partials/scripts.html b/layouts/partials/scripts.html deleted file mode 100644 index 1560470..0000000 --- a/layouts/partials/scripts.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ if .Params.showTableOfContents | default (.Site.Params.article.showTableOfContents | default false) }} - -{{ end }} \ No newline at end of file