toc-thumb.tsx74 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { useEffectEvent, useOnChange } from 'common' |
| 4 | import { useEffect, useRef, type HTMLAttributes, type RefObject } from 'react' |
| 5 | |
| 6 | import * as Primitive from './toc-primitive' |
| 7 | |
| 8 | export type TOCThumb = [top: number, height: number] |
| 9 | |
| 10 | function 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 | |
| 34 | function 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 | |
| 39 | export 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 | } |