From 9f5ccddad2e73edadb8561d8e12973bdf3ecc83e Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Fri, 6 Sep 2024 15:11:46 -0700 Subject: [PATCH] Improve markdown TOC scrolling (#349) Use a better system for scrolling using scrollTo on the OverlayScrollbars ref. This lets me get the heading as close to the top of the viewport as possible without the convoluted CSS tricks I was trying before. --- frontend/app/element/markdown.less | 2 - frontend/app/element/markdown.tsx | 65 +++++++++++++++--------------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/frontend/app/element/markdown.less b/frontend/app/element/markdown.less index 6e4cbcc05..e98cb2395 100644 --- a/frontend/app/element/markdown.less +++ b/frontend/app/element/markdown.less @@ -17,7 +17,6 @@ font-size: 14px; overflow-wrap: break-word; margin-bottom: 10px; - --half-contents-height: 10em; .heading { &:first-of-type { @@ -26,7 +25,6 @@ color: var(--app-text-color); margin-top: 16px; margin-bottom: 8px; - scroll-margin-block-end: var(--half-contents-height); } strong { diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx index 8caaf0974..bb33e5ac5 100644 --- a/frontend/app/element/markdown.tsx +++ b/frontend/app/element/markdown.tsx @@ -8,13 +8,12 @@ import * as util from "@/util/util"; import { useAtomValueSafe } from "@/util/util"; import { clsx } from "clsx"; import { Atom } from "jotai"; -import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; -import React, { CSSProperties, useCallback, useMemo, useRef } from "react"; +import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; +import React, { CSSProperties, useRef } from "react"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc"; import remarkGfm from "remark-gfm"; -import { useHeight } from "../hook/useHeight"; import "./markdown.less"; const Link = ({ href, children }: { href: string; children: React.ReactNode }) => { @@ -148,23 +147,23 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts, const textAtomValue = useAtomValueSafe(textAtom); const tocRef = useRef([]); const showToc = useAtomValueSafe(showTocAtom) ?? false; - const contentsRef = useRef(null); - const contentsHeight = useHeight(contentsRef, 200); + const contentsOsRef = useRef(null); - const halfContentsHeight = useMemo(() => { - return `${contentsHeight / 2}px`; - }, [contentsHeight]); - - const onTocClick = useCallback((data: string) => { - if (contentsRef.current) { - const headings = contentsRef.current.getElementsByClassName("heading"); + const onTocClick = (data: string) => { + if (contentsOsRef.current && contentsOsRef.current.osInstance()) { + const { viewport } = contentsOsRef.current.osInstance().elements(); + const headings = viewport.getElementsByClassName("heading"); for (const heading of headings) { if (heading.textContent === data) { - heading.scrollIntoView({ inline: "nearest", block: "end" }); + const headingBoundingRect = heading.getBoundingClientRect(); + const viewportBoundingRect = viewport.getBoundingClientRect(); + const headingTop = headingBoundingRect.top - viewportBoundingRect.top; + viewport.scrollBy({ top: headingTop }); + break; } } } - }, []); + }; const markdownComponents = { a: Link, @@ -180,30 +179,19 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts, pre: (props: any) => , }; - const toc = useMemo(() => { - if (showToc && tocRef.current.length > 0) { - return tocRef.current.map((item) => { - return ( - onTocClick(item.value)} - > - {item.value} - - ); - }); - } - }, [showToc, tocRef]); + // const toc = useMemo(() => { + // if (showToc && tocRef.current.length > 0) { + // return + // } + // }, [showToc, tocRef]); text = textAtomValue ?? text; return ( -
+

Table of Contents

- {toc} + {tocRef.current.map((item) => { + return ( + onTocClick(item.value)} + > + {item.value} + + ); + })}
)}