|
|
|
|
@ -10,6 +10,7 @@
@@ -10,6 +10,7 @@
|
|
|
|
|
SidebarItem, |
|
|
|
|
} from "flowbite-svelte"; |
|
|
|
|
import Self from "./TableOfContents.svelte"; |
|
|
|
|
import { onMount, onDestroy } from "svelte"; |
|
|
|
|
|
|
|
|
|
let { depth, onSectionFocused } = $props<{ |
|
|
|
|
rootAddress: string; |
|
|
|
|
@ -32,6 +33,10 @@
@@ -32,6 +33,10 @@
|
|
|
|
|
return newEntries; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Track the currently visible section |
|
|
|
|
let currentVisibleSection = $state<string | null>(null); |
|
|
|
|
let observer: IntersectionObserver; |
|
|
|
|
|
|
|
|
|
function setEntryExpanded(address: string, expanded: boolean = false) { |
|
|
|
|
const entry = toc.getEntry(address); |
|
|
|
|
if (!entry) { |
|
|
|
|
@ -41,6 +46,100 @@
@@ -41,6 +46,100 @@
|
|
|
|
|
toc.expandedMap.set(address, expanded); |
|
|
|
|
entry.resolveChildren(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function handleSectionClick(address: string) { |
|
|
|
|
// Smooth scroll to the section |
|
|
|
|
const element = document.getElementById(address); |
|
|
|
|
if (element) { |
|
|
|
|
element.scrollIntoView({ |
|
|
|
|
behavior: 'smooth', |
|
|
|
|
block: 'start', |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
onSectionFocused?.(address); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check if an entry is currently visible |
|
|
|
|
function isEntryVisible(address: string): boolean { |
|
|
|
|
return currentVisibleSection === address; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Set up intersection observer to track visible sections |
|
|
|
|
onMount(() => { |
|
|
|
|
observer = new IntersectionObserver( |
|
|
|
|
(entries) => { |
|
|
|
|
// Find the section that is most visible in the viewport |
|
|
|
|
let maxIntersectionRatio = 0; |
|
|
|
|
let mostVisibleSection: string | null = null; |
|
|
|
|
|
|
|
|
|
entries.forEach((entry) => { |
|
|
|
|
if (entry.isIntersecting && entry.intersectionRatio > maxIntersectionRatio) { |
|
|
|
|
maxIntersectionRatio = entry.intersectionRatio; |
|
|
|
|
mostVisibleSection = entry.target.id; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (mostVisibleSection && mostVisibleSection !== currentVisibleSection) { |
|
|
|
|
currentVisibleSection = mostVisibleSection; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
threshold: [0, 0.25, 0.5, 0.75, 1], |
|
|
|
|
rootMargin: "-20% 0px -20% 0px", // Consider section visible when it's in the middle 60% of the viewport |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Function to observe all section elements |
|
|
|
|
function observeSections() { |
|
|
|
|
const sections = document.querySelectorAll('section[id]'); |
|
|
|
|
sections.forEach((section) => { |
|
|
|
|
observer.observe(section); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Initial observation |
|
|
|
|
observeSections(); |
|
|
|
|
|
|
|
|
|
// Set up a mutation observer to watch for new sections being added |
|
|
|
|
const mutationObserver = new MutationObserver((mutations) => { |
|
|
|
|
mutations.forEach((mutation) => { |
|
|
|
|
mutation.addedNodes.forEach((node) => { |
|
|
|
|
if (node.nodeType === Node.ELEMENT_NODE) { |
|
|
|
|
const element = node as Element; |
|
|
|
|
// Check if the added node is a section with an id |
|
|
|
|
if (element.tagName === 'SECTION' && element.id) { |
|
|
|
|
observer.observe(element); |
|
|
|
|
} |
|
|
|
|
// Check if the added node contains sections |
|
|
|
|
const sections = element.querySelectorAll?.('section[id]'); |
|
|
|
|
if (sections) { |
|
|
|
|
sections.forEach((section) => { |
|
|
|
|
observer.observe(section); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Start observing the document body for changes |
|
|
|
|
mutationObserver.observe(document.body, { |
|
|
|
|
childList: true, |
|
|
|
|
subtree: true, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
observer.disconnect(); |
|
|
|
|
mutationObserver.disconnect(); |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
onDestroy(() => { |
|
|
|
|
if (observer) { |
|
|
|
|
observer.disconnect(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<!-- TODO: Figure out how to style indentations. --> |
|
|
|
|
@ -50,18 +149,20 @@
@@ -50,18 +149,20 @@
|
|
|
|
|
{@const address = entry.address} |
|
|
|
|
{@const expanded = toc.expandedMap.get(address) ?? false} |
|
|
|
|
{@const isLeaf = toc.leaves.has(address)} |
|
|
|
|
{@const isVisible = isEntryVisible(address)} |
|
|
|
|
{#if isLeaf} |
|
|
|
|
<SidebarItem |
|
|
|
|
label={entry.title} |
|
|
|
|
href={`#${address}`} |
|
|
|
|
spanClass="px-2 text-ellipsis" |
|
|
|
|
onclick={() => onSectionFocused?.(address)} |
|
|
|
|
class={isVisible ? "toc-highlight" : ""} |
|
|
|
|
onclick={() => handleSectionClick(address)} |
|
|
|
|
/> |
|
|
|
|
{:else} |
|
|
|
|
{@const childDepth = depth + 1} |
|
|
|
|
<SidebarDropdownWrapper |
|
|
|
|
label={entry.title} |
|
|
|
|
btnClass="flex items-center p-2 w-full font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-primary-50 dark:text-white dark:hover:bg-primary-800" |
|
|
|
|
btnClass="flex items-center p-2 w-full font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-primary-50 dark:text-white dark:hover:bg-primary-800 {isVisible ? 'toc-highlight' : ''}" |
|
|
|
|
bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)} |
|
|
|
|
> |
|
|
|
|
<Self rootAddress={address} depth={childDepth} {onSectionFocused} /> |
|
|
|
|
|