-
Alexander Schoch authoredAlexander Schoch authored
toc.jsx 3.13 KiB
import { useState, useEffect } from "react";
import {
createStyles,
Box,
Text,
Group,
rem,
useMantineTheme,
} from "@mantine/core";
import { Icon, ICONS } from "vseth-canine-ui";
const useStyles = createStyles((theme) => ({
link: {
...theme.fn.focusStyles(),
display: "block",
textDecoration: "none",
color: theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black,
lineHeight: 1.2,
fontSize: theme.fontSizes.sm,
padding: "5px",
borderTopRightRadius: theme.radius.lg,
borderBottomRightRadius: theme.radius.lg,
borderLeft: `2px solid ${
theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[3]
}`,
"&:hover": {
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[0],
},
},
linkActive: {
fontWeight: 1000,
borderLeftColor:
theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 6 : 7],
color:
theme.colors[theme.primaryColor][theme.colorScheme === "dark" ? 2 : 7],
"&, &:hover": {
backgroundColor:
theme.colorScheme === "dark"
? theme.fn.rgba(theme.colors[theme.primaryColor][9], 0.25)
: theme.colors[theme.primaryColor][0],
},
},
}));
export default function TOC() {
const [items, setItems] = useState([]);
const [active, setActive] = useState("");
const theme = useMantineTheme();
const { classes, cx } = useStyles();
const findClosestHeading = () => {
const headings = document.querySelectorAll("h1, h2");
const currentPosition = window.scrollY;
let closestHeading = null;
let closestDistance = Infinity;
headings.forEach((heading) => {
const headingPosition =
heading.getBoundingClientRect().top + window.scrollY;
const distance = Math.abs(currentPosition - headingPosition);
if (distance < closestDistance) {
closestHeading = heading;
closestDistance = distance;
}
});
return closestHeading;
};
useEffect(() => {
const headings = [...document.querySelectorAll("h1, h2, h3")];
const links = headings.map((item) => ({
link: "#" + item.id,
label: item.innerText,
order: Number(item.localName[item.localName.length - 1]),
}));
setItems(links);
setActive("#" + findClosestHeading().id);
document.addEventListener("scroll", function (e) {
setActive("#" + findClosestHeading().id);
});
}, []);
const entries = items.map((item) => (
<Box
component="a"
href={item.link}
key={item.label}
className={cx(classes.link, {
[classes.linkActive]: active === item.link,
})}
sx={(theme) => ({
paddingLeft: `calc(${item.order} * ${theme.spacing.md}px)`,
})}
>
{item.label}
</Box>
));
return (
<div
style={{
position: "sticky",
top: "2rem",
maxHeight: "calc(100vh - 2rem)",
overflowY: "auto",
marginBottom: "20px",
}}
>
<Group mb="md">
<Icon icon={ICONS.LIST} />
<Text>Table of contents</Text>
</Group>
{entries}
</div>
);
}