clone of repo on github
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

182 lines
5.5 KiB

<script lang="ts">
import {
TableOfContents,
type TocEntry,
} from "$lib/components/publications/table_of_contents.svelte";
import { getContext } from "svelte";
import {
SidebarDropdownWrapper,
SidebarGroup,
SidebarItem,
} from "flowbite-svelte";
import Self from "./TableOfContents.svelte";
import { onMount, onDestroy } from "svelte";
let { depth, onSectionFocused, onLoadMore, toc } = $props<{
rootAddress: string;
depth: number;
toc: TableOfContents;
onSectionFocused?: (address: string) => void;
onLoadMore?: () => void;
}>();
let entries = $derived.by<TocEntry[]>(() => {
const newEntries = [];
for (const [_, entry] of toc.addressMap) {
if (entry.depth !== depth) {
continue;
}
newEntries.push(entry);
}
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) {
return;
}
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 this is the last entry and trigger loading more events
const currentEntries = entries;
const lastEntry = currentEntries[currentEntries.length - 1];
if (lastEntry && lastEntry.address === address) {
console.debug('[TableOfContents] Last entry clicked, triggering load more');
onLoadMore?.();
}
}
// 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. -->
<!-- TODO: Make group title fonts the same as entry title fonts. -->
<SidebarGroup>
{#each entries as entry, index}
{@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"
class={`${isVisible ? "toc-highlight" : ""} `}
onclick={() => handleSectionClick(address)}
>
<!-- Empty for now - could add icons or labels here in the future -->
</SidebarItem>
{: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 {isVisible ? 'toc-highlight' : ''} "
bind:isOpen={() => expanded, (open) => setEntryExpanded(address, open)}
>
<Self rootAddress={address} depth={childDepth} {toc} {onSectionFocused} {onLoadMore} />
</SidebarDropdownWrapper>
{/if}
{/each}
</SidebarGroup>