-
-
Notifications
You must be signed in to change notification settings - Fork 458
Expand file tree
/
Copy pathtable-of-content.tsx
More file actions
125 lines (107 loc) · 2.64 KB
/
Copy pathtable-of-content.tsx
File metadata and controls
125 lines (107 loc) · 2.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
type Item = {
title: string
url: string
items?: Item[]
}
type Items = {
items?: Item[]
}
export type TableOfContents = Items
export type TocItem = Item
type TocProps = {
toc: TableOfContents
}
export function TableOfContent({ toc }: TocProps) {
const itemIds = React.useMemo(
() =>
toc.items
? toc.items
.flatMap(item => [item.url, item?.items?.map(item => item.url)])
.flat()
.filter(Boolean)
.map(id => id?.split('#')[1])
: [],
[toc],
)
const activeHeading = useActiveItem(itemIds)
if (!toc?.items) {
return null
}
return (
<div className="space-y-2">
<p className="font-medium">On This Page</p>
<Tree tree={toc} activeItem={activeHeading} />
</div>
)
}
// TODO: use useIntersectionObserver from usehooks-ts
function useActiveItem(itemIds: (string | undefined)[]) {
const [activeId, setActiveId] = React.useState<string>('')
React.useEffect(() => {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
})
},
{ rootMargin: `0% 0% -80% 0%` },
)
itemIds?.forEach(id => {
if (!id) {
return
}
const element = document.getElementById(id)
if (element) {
observer.observe(element)
}
})
return () => {
itemIds?.forEach(id => {
if (!id) {
return
}
const element = document.getElementById(id)
if (element) {
observer.unobserve(element)
}
})
}
}, [itemIds])
return activeId
}
type TreeProps = {
tree: TableOfContents
level?: number
activeItem?: string | null
}
function Tree({ tree, level = 1, activeItem }: TreeProps) {
return tree?.items?.length && level < 3 ? (
<ul className={cn('m-0 list-none', { 'pl-4': level !== 1 })}>
{tree.items.map((item, index) => {
return (
<li key={index} className={cn('mt-0 pt-2')}>
<a
href={item.url}
className={cn(
'inline-block no-underline',
item.url === `#${activeItem ?? ''}`
? 'font-medium text-primary'
: 'text-sm text-muted-foreground',
)}
>
{item.title}
</a>
{item.items?.length ? (
<Tree tree={item} level={level + 1} activeItem={activeItem} />
) : null}
</li>
)
})}
</ul>
) : null
}