toc-thumb.tsx74 lines · main
1'use client'
2
3import { useEffectEvent, useOnChange } from 'common'
4import { useEffect, useRef, type HTMLAttributes, type RefObject } from 'react'
5
6import * as Primitive from './toc-primitive'
7
8export type TOCThumb = [top: number, height: number]
9
10function calc(container: HTMLElement, active: string[]): TOCThumb {
11 if (active.length === 0 || container.clientHeight === 0) {
12 return [0, 0]
13 }
14
15 let upper = Number.MAX_VALUE,
16 lower = 0
17
18 for (const item of active) {
19 const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`)
20
21 if (!element) continue
22
23 const styles = getComputedStyle(element)
24 upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop))
25 lower = Math.max(
26 lower,
27 element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom)
28 )
29 }
30
31 return [upper, lower - upper]
32}
33
34function update(element: HTMLElement, info: TOCThumb): void {
35 element.style.setProperty('--toc-top', `${info[0]}px`)
36 element.style.setProperty('--toc-height', `${info[1]}px`)
37}
38
39export function TocThumb({
40 containerRef,
41 ...props
42}: HTMLAttributes<HTMLDivElement> & {
43 containerRef: RefObject<HTMLElement | null>
44}) {
45 const active = Primitive.useActiveAnchors()
46 const thumbRef = useRef<HTMLDivElement>(null)
47
48 const onResize = useEffectEvent(() => {
49 if (!containerRef.current || !thumbRef.current) return
50
51 update(thumbRef.current, calc(containerRef.current, active))
52 })
53
54 useEffect(() => {
55 if (!containerRef.current) return
56 const container = containerRef.current
57
58 onResize()
59 const observer = new ResizeObserver(onResize)
60 observer.observe(container)
61
62 return () => {
63 observer.disconnect()
64 }
65 }, [containerRef, onResize])
66
67 useOnChange(active, () => {
68 if (!containerRef.current || !thumbRef.current) return
69
70 update(thumbRef.current, calc(containerRef.current, active))
71 })
72
73 return <div ref={thumbRef} role="none" {...props} />
74}