import { Button , Heading , VStack } from "@chakra-ui/react" ; import GithubSlugger from "github-slugger" ; import React , { Dispatch , FC , SetStateAction , useEffect , useRef , useState , } from "react" ; interface IProps { source : string ; } const useIntersectionObserver = ( setActiveId ) => { const headingElementsRef = useRef ( { } ) ; useEffect ( ( ) => { const callback = ( headings ) => { headingElementsRef . current = headings . reduce ( ( map , headingElement ) => { map [ headingElement . target . id ] = headingElement ; return map ; } , headingElementsRef . current ) ; const visibleHeadings = [ ] ; Object . keys ( headingElementsRef . current ) . forEach ( ( key ) => { const headingElement = headingElementsRef . current [ key ] ; if ( headingElement . isIntersecting ) visibleHeadings . push ( headingElement ) ; } ) ; const getIndexFromId = ( id ) => headingElements . findIndex ( ( heading ) => heading . id === id ) ; if ( visibleHeadings . length === 1 ) { setActiveId ( visibleHeadings [ 0 ] . target . id ) ; } else if ( visibleHeadings . length > 1 ) { const sortedVisibleHeadings = visibleHeadings . sort ( ( a , b ) => getIndexFromId ( b . target . id ) - getIndexFromId ( a . target . id ) ) ; setActiveId ( sortedVisibleHeadings [ 0 ] . target . id ) ; } } ; const observer = new IntersectionObserver ( callback , { rootMargin : "0px 0px -70% 0px" , } ) ; const headingElements = Array . from ( document . querySelectorAll ( ".article h2, .article h3" ) ) ; headingElements . forEach ( ( element ) => observer . observe ( element ) ) ; return ( ) => observer . disconnect ( ) ; } , [ setActiveId ] ) ; } ; const TableOfContents = ( { source } ) => { const headingLines = source . split ( "\n" ) . filter ( ( line ) => line . match ( /^###*\s/ ) ) ; const headings = headingLines . map ( ( raw ) => { const text = raw . replace ( /^###*\s/ , "" ) ; const level = raw . slice ( 0 , 3 ) === "###" ? 3 : 2 ; const slugger = new GithubSlugger ( ) ; return { text , level , href : slugger . slug ( text ) , } ; } ) ; const [ activeId , setActiveId ] = useState < string > ( ) ; useIntersectionObserver ( setActiveId ) ; return ( < VStack alignItems = "left" > < Heading size = "sm" > Table of contents < /Heading> < VStack spacing = { 2 } alignItems = "left" > { headings . map ( ( heading , index ) => { return ( < Button key = { index } variant = "link" justifyContent = "left" color = "gray.400" fontSize = "sm" pl = { ( heading . level - 2 ) * 4 } _hover = { { color : "blue.400" , } } _focus = { { } } onClick = { ( e ) => { e . preventDefault ( ) ; document . querySelector ( `#${ heading . href }` ) . scrollIntoView ( { behavior : "smooth" , block : "start" , inline : "nearest" , } ) ; } } fontWeight = { heading . href === activeId ? "bold" : "normal" } > { heading . text } < /Button>); } ) } < /VStack> < /VStack>); } ; export default TableOfContents ;