UNPKG

fumadocs-core

Version:

The React.js library for building a documentation website

138 lines (134 loc) 4.22 kB
'use client'; import { createContext, useContext, useEffect, useEffectEvent, useLayoutEffect, useMemo, useRef, useState } from "react"; import { jsx } from "react/jsx-runtime"; import scrollIntoView from "scroll-into-view-if-needed"; //#region src/utils/merge-refs.ts function mergeRefs(...refs) { return (value) => { refs.forEach((ref) => { if (typeof ref === "function") ref(value); else if (ref != null) ref.current = value; }); }; } //#endregion //#region src/toc.tsx const ActiveAnchorContext = createContext([]); const ScrollContext = createContext({ current: null }); /** * The estimated active heading ID */ function useActiveAnchor() { return useContext(ActiveAnchorContext)[0]; } /** * The id of visible anchors */ function useActiveAnchors() { return useContext(ActiveAnchorContext); } function ScrollProvider({ containerRef, children }) { return /* @__PURE__ */ jsx(ScrollContext.Provider, { value: containerRef, children }); } function AnchorProvider({ toc, single = false, children }) { const headings = useMemo(() => { return toc.map((item) => item.url.split("#")[1]); }, [toc]); return /* @__PURE__ */ jsx(ActiveAnchorContext.Provider, { value: useAnchorObserver(headings, single), children }); } function TOCItem({ ref, onActiveChange = () => null, ...props }) { const containerRef = useContext(ScrollContext); const anchorRef = useRef(null); const activeOrder = useActiveAnchors().indexOf(props.href.slice(1)); const isActive = activeOrder !== -1; const shouldScroll = activeOrder === 0; const onActiveChangeEvent = useEffectEvent(onActiveChange); useLayoutEffect(() => { const anchor = anchorRef.current; const container = containerRef.current; if (container && anchor && shouldScroll) scrollIntoView(anchor, { behavior: "smooth", block: "center", inline: "center", scrollMode: "always", boundary: container }); }, [containerRef, shouldScroll]); useEffect(() => { return () => onActiveChangeEvent(isActive); }, [isActive]); return /* @__PURE__ */ jsx("a", { ref: mergeRefs(anchorRef, ref), "data-active": isActive, ...props, children: props.children }); } /** * Find the active heading of page * * It selects the top heading by default, and the last item when reached the bottom of page. * * @param watch - An array of element ids to watch * @param single - only one active item at most * @returns Active anchor */ function useAnchorObserver(watch, single) { const observerRef = useRef(null); const [activeAnchor, setActiveAnchor] = useState(() => []); const stateRef = useRef(null); const onChange = useEffectEvent((entries) => { stateRef.current ??= { visible: /* @__PURE__ */ new Set() }; const state = stateRef.current; for (const entry of entries) if (entry.isIntersecting) state.visible.add(entry.target.id); else state.visible.delete(entry.target.id); if (state.visible.size === 0) { const viewTop = entries.length > 0 ? entries[0]?.rootBounds?.top ?? 0 : 0; let fallback; let min = -1; for (const id of watch) { const element = document.getElementById(id); if (!element) continue; const d = Math.abs(viewTop - element.getBoundingClientRect().top); if (min === -1 || d < min) { fallback = element; min = d; } } setActiveAnchor(fallback ? [fallback.id] : []); } else { const items = watch.filter((item) => state.visible.has(item)); setActiveAnchor(single ? items.slice(0, 1) : items); } }); useEffect(() => { if (observerRef.current) return; observerRef.current = new IntersectionObserver(onChange, { rootMargin: "0px", threshold: .98 }); return () => { observerRef.current?.disconnect(); observerRef.current = null; }; }, []); useEffect(() => { const observer = observerRef.current; if (!observer) return; const elements = watch.flatMap((heading) => document.getElementById(heading) ?? []); for (const element of elements) observer.observe(element); return () => { for (const element of elements) observer.unobserve(element); }; }, [watch]); return activeAnchor; } //#endregion export { AnchorProvider, ScrollProvider, TOCItem, useActiveAnchor, useActiveAnchors }; //# sourceMappingURL=toc.js.map