diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx index cf0adf9f9..31d507693 100644 --- a/frontend/app/element/markdown.tsx +++ b/frontend/app/element/markdown.tsx @@ -11,6 +11,7 @@ import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overl import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; +import rehypeSlug from "rehype-slug"; import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc"; import remarkGfm from "remark-gfm"; import "./markdown.less"; @@ -24,8 +25,12 @@ const Link = ({ href, children }: { href: string; children: React.ReactNode }) = ); }; -const Heading = ({ children, hnum }: { children: React.ReactNode; hnum: number }) => { - return
{children}
; +const Heading = ({ id, children, hnum }: { id?: string; children: React.ReactNode; hnum: number }) => { + return ( +
+ {children} +
+ ); }; const Code = ({ children }: { children: React.ReactNode }) => { @@ -148,30 +153,30 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts, const showToc = useAtomValueSafe(showTocAtom) ?? false; const contentsOsRef = useRef(null); - const onTocClick = useCallback((data: string) => { + // Ensure uniqueness of ids between MD preview instances. + const [idPrefix] = useState(crypto.randomUUID()); + + const onTocClick = useCallback((href: 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) { - const headingBoundingRect = heading.getBoundingClientRect(); - const viewportBoundingRect = viewport.getBoundingClientRect(); - const headingTop = headingBoundingRect.top - viewportBoundingRect.top; - viewport.scrollBy({ top: headingTop }); - break; - } + const heading = document.getElementById(idPrefix + href.slice(1)); + if (heading) { + const headingBoundingRect = heading.getBoundingClientRect(); + const viewportBoundingRect = viewport.getBoundingClientRect(); + const headingTop = headingBoundingRect.top - viewportBoundingRect.top; + viewport.scrollBy({ top: headingTop }); } } }, []); const markdownComponents = { a: Link, - h1: (props: any) => , - h2: (props: any) => , - h3: (props: any) => , - h4: (props: any) => , - h5: (props: any) => , - h6: (props: any) => , + h1: (props: any) => , + h2: (props: any) => , + h3: (props: any) => , + h4: (props: any) => , + h5: (props: any) => , + h6: (props: any) => , img: (props: any) => , source: (props: any) => , code: Code, @@ -186,7 +191,7 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts, key={item.href} className="toc-item" style={{ "--indent-factor": item.depth } as React.CSSProperties} - onClick={() => onTocClick(item.value)} + onClick={() => onTocClick(item.href)} > {item.value} @@ -206,7 +211,7 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts, > {text} diff --git a/package.json b/package.json index 6237b86cf..936b9e59b 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "react-gauge-chart": "^0.5.1", "react-markdown": "^9.0.1", "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", "remark-flexible-toc": "^1.1.1", "remark-gfm": "^4.0.0", "rxjs": "^7.8.1", diff --git a/yarn.lock b/yarn.lock index c1e27f23b..19a7c2982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12181,6 +12181,7 @@ __metadata: react-gauge-chart: "npm:^0.5.1" react-markdown: "npm:^9.0.1" rehype-raw: "npm:^7.0.0" + rehype-slug: "npm:^6.0.0" remark-flexible-toc: "npm:^1.1.1" remark-gfm: "npm:^4.0.0" rollup-plugin-flow: "npm:^1.1.1"