mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
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.
This commit is contained in:
parent
f07ac79b0b
commit
9f5ccddad2
2 changed files with 32 additions and 35 deletions
|
|
@ -17,7 +17,6 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
--half-contents-height: 10em;
|
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
|
|
@ -26,7 +25,6 @@
|
||||||
color: var(--app-text-color);
|
color: var(--app-text-color);
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
scroll-margin-block-end: var(--half-contents-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,12 @@ import * as util from "@/util/util";
|
||||||
import { useAtomValueSafe } from "@/util/util";
|
import { useAtomValueSafe } from "@/util/util";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { Atom } from "jotai";
|
import { Atom } from "jotai";
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||||
import React, { CSSProperties, useCallback, useMemo, useRef } from "react";
|
import React, { CSSProperties, useRef } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
|
import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { useHeight } from "../hook/useHeight";
|
|
||||||
import "./markdown.less";
|
import "./markdown.less";
|
||||||
|
|
||||||
const Link = ({ href, children }: { href: string; children: React.ReactNode }) => {
|
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 textAtomValue = useAtomValueSafe(textAtom);
|
||||||
const tocRef = useRef<TocItem[]>([]);
|
const tocRef = useRef<TocItem[]>([]);
|
||||||
const showToc = useAtomValueSafe(showTocAtom) ?? false;
|
const showToc = useAtomValueSafe(showTocAtom) ?? false;
|
||||||
const contentsRef = useRef<HTMLDivElement>(null);
|
const contentsOsRef = useRef<OverlayScrollbarsComponentRef>(null);
|
||||||
const contentsHeight = useHeight(contentsRef, 200);
|
|
||||||
|
|
||||||
const halfContentsHeight = useMemo(() => {
|
const onTocClick = (data: string) => {
|
||||||
return `${contentsHeight / 2}px`;
|
if (contentsOsRef.current && contentsOsRef.current.osInstance()) {
|
||||||
}, [contentsHeight]);
|
const { viewport } = contentsOsRef.current.osInstance().elements();
|
||||||
|
const headings = viewport.getElementsByClassName("heading");
|
||||||
const onTocClick = useCallback((data: string) => {
|
|
||||||
if (contentsRef.current) {
|
|
||||||
const headings = contentsRef.current.getElementsByClassName("heading");
|
|
||||||
for (const heading of headings) {
|
for (const heading of headings) {
|
||||||
if (heading.textContent === data) {
|
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 = {
|
const markdownComponents = {
|
||||||
a: Link,
|
a: Link,
|
||||||
|
|
@ -180,30 +179,19 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts,
|
||||||
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const toc = useMemo(() => {
|
// const toc = useMemo(() => {
|
||||||
if (showToc && tocRef.current.length > 0) {
|
// if (showToc && tocRef.current.length > 0) {
|
||||||
return tocRef.current.map((item) => {
|
// return
|
||||||
return (
|
// }
|
||||||
<a
|
// }, [showToc, tocRef]);
|
||||||
key={item.href}
|
|
||||||
className="toc-item"
|
|
||||||
style={{ "--indent-factor": item.depth } as CSSProperties}
|
|
||||||
onClick={() => onTocClick(item.value)}
|
|
||||||
>
|
|
||||||
{item.value}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [showToc, tocRef]);
|
|
||||||
|
|
||||||
text = textAtomValue ?? text;
|
text = textAtomValue ?? text;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("markdown", className)} style={style} ref={contentsRef}>
|
<div className={clsx("markdown", className)} style={style}>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
|
ref={contentsOsRef}
|
||||||
className="content"
|
className="content"
|
||||||
style={{ "--half-contents-height": halfContentsHeight } as CSSProperties}
|
|
||||||
options={{ scrollbars: { autoHide: "leave" } }}
|
options={{ scrollbars: { autoHide: "leave" } }}
|
||||||
>
|
>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|
@ -218,7 +206,18 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts,
|
||||||
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>
|
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>
|
||||||
<div className="toc-inner">
|
<div className="toc-inner">
|
||||||
<h4>Table of Contents</h4>
|
<h4>Table of Contents</h4>
|
||||||
{toc}
|
{tocRef.current.map((item) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={item.href}
|
||||||
|
className="toc-item"
|
||||||
|
style={{ "--indent-factor": item.depth } as CSSProperties}
|
||||||
|
onClick={() => onTocClick(item.value)}
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue