-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Expand file tree
/
Copy pathuseTableOfContents.tsx
More file actions
112 lines (103 loc) · 4.35 KB
/
Copy pathuseTableOfContents.tsx
File metadata and controls
112 lines (103 loc) · 4.35 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
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export interface TableOfContent {
index: number
text: string
marginLeft: number
element: HTMLElement
}
const getHeaderScrollOffset = () => {
const rawValue = getComputedStyle(document.documentElement)
.getPropertyValue('--header-scroll-offset')
.trim()
const offset = Number.parseFloat(rawValue)
return Number.isFinite(offset) ? offset : 0
}
const useTableOfContents = (selector: string) => {
const intersectingListRef = useRef<boolean[]>([]) // isIntersecting array
const [tableOfContents, setTableOfContents] = useState<TableOfContent[]>([])
const [activeIndex, setActiveIndex] = useState(0)
const { t } = useTranslation()
const io = useRef<IntersectionObserver | null>(null);
const [ref, setRef] = useState("-1")
const lastRef = useRef("")
useEffect(() => {
if (lastRef.current === ref) return
const content = document.querySelector(selector)
if (!content) return
const intersectingList = intersectingListRef.current
const headers = content.querySelectorAll<HTMLElement>(
'h1, h2, h3, h4, h5, h6'
) // all headers
// set TableOfContents
const tocData = Array.from(headers).map<TableOfContent>((header, i) => ({
index: i,
text: header.textContent || '',
marginLeft: (Number(header.tagName.charAt(1)) - 1) * 10,
element: header, // have to down little bit
}))
setTableOfContents(tocData)
// create IntersectionObserver
if (io.current) io.current.disconnect()
io.current = new IntersectionObserver(
(entries) => {
// save isIntersecting info to array using data-id
entries.forEach(({ target, isIntersecting }) => {
const idx = Number((target as HTMLElement).dataset.id || 0)
intersectingList[idx] = isIntersecting
})
// get activeIndex
const currentIndex = intersectingList.findIndex((item) => item)
let activeIndex = currentIndex - 1
if (currentIndex === -1) {
activeIndex = intersectingList.length - 1
} else if (currentIndex === 0) {
activeIndex = 0
}
setActiveIndex(activeIndex)
},
{ rootMargin: "-20% 0px 10000px 0px", threshold: 0 }
)
intersectingList.length = 0 // reset array
headers.forEach((header, i) => {
if (header.getAttribute('data-id') !== null) return
header.setAttribute('data-id', i.toString()) // set data-id
intersectingList.push(false) // increase array length
io.current!.observe(header) // register to observe
})
lastRef.current = ref
return () => {
if (io.current) io.current.disconnect()
}
}, [ref])
const cleanup = (newId: string) => {
if (lastRef.current === newId) return
setRef(newId)
if (io.current) io.current.disconnect()
}
return {
TOC: () => (<div className='rounded-2xl bg-w py-4 px-4 t-primary'>
<h2 className="text-lg font-bold">{t("index.title")}</h2>
<ul className="max-h-[calc(100vh-10.25rem)] overflow-auto" style={{ scrollbarWidth: "none" }}>
{tableOfContents.length === 0 && <li>{t("index.empty.title")}</li>}
{tableOfContents.map((item) => (
<li
key={`toc$${item.index}`}
className={`cursor-pointer hover:opacity-50 ${activeIndex === item.index ? "text-theme" : ""}`}
style={{ marginLeft: item.marginLeft }}
onClick={() => {
const top = item.element.getBoundingClientRect().top + window.scrollY - getHeaderScrollOffset()
window.scrollTo({
top: Math.max(top, 0),
behavior: 'smooth'
})
}}
>
{item.text}
</li>
))}
</ul>
</div>), cleanup
}
}
export default useTableOfContents