Skip to content

Commit

Permalink
refactor: 自己实现目录导航组件 #29
Browse files Browse the repository at this point in the history
  • Loading branch information
Mereithhh committed Sep 5, 2022
1 parent 1d2f9e4 commit 7648c1e
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 76 deletions.
14 changes: 14 additions & 0 deletions packages/website/components/Markdown/heading.tsx
@@ -0,0 +1,14 @@
import React from "react";
import { HeadingProps } from "react-markdown/lib/ast-to-react";

export const HeadingRender = (props: HeadingProps) => {
const { node, children } = props;
let text = "";
try {
text = children[0] as string;
} catch (err) {
text = "";
}

return React.createElement(node.tagName, { ["data-id"]: text }, text);
};
7 changes: 7 additions & 0 deletions packages/website/components/Markdown/index.tsx
Expand Up @@ -10,6 +10,7 @@ import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import ImageBox from "../ImageBox";
import dynamic from "next/dynamic";
import { HeadingRender } from "./heading";
export default function (props: { content: string }) {
return (
<>
Expand Down Expand Up @@ -83,6 +84,12 @@ export default function (props: { content: string }) {
</code>
);
},
h1: HeadingRender,
h2: HeadingRender,
h3: HeadingRender,
h4: HeadingRender,
h5: HeadingRender,
h6: HeadingRender,
img(props) {
return (
<ImageBox
Expand Down
101 changes: 101 additions & 0 deletions packages/website/components/MarkdownTocBar/core.tsx
@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
import throttle from "lodash/throttle";
import { NavItem } from "./tools";

import scroll from "react-scroll";
export default function (props: { items: NavItem[]; headingOffset: number }) {
const { items } = props;
const [currIndex, setCurrIndex] = useState(-1);
const handleScroll = throttle((ev: Event) => {
ev.stopPropagation();
ev.preventDefault();

let top: any = null;
let topEl: any = null;
let lastMin = 9999999999;
for (const each of items) {
const el: any = document.querySelector(`[data-id="${each.text}"]`);

if (!topEl) {
top = each;
topEl = el;
}
if (el) {
const scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0;
const v = Math.abs(scrollTop + props.headingOffset - el.offsetTop);
if (v <= lastMin) {
lastMin = v;
top = each;
topEl = el;
}
}
}
setCurrIndex(top.index);

// updateHash(top.text);
}, 100);
useEffect(() => {
const el = document.querySelector(".markdown-navigation div.active");
if (el) {
let to = (el as any)?.offsetTop;
if (to <= props.headingOffset) {
to = 0;
}
scroll.animateScroll.scrollTo(to, {
containerId: "toc-container",
smooth: true,
delay: 0,
spyThrottle: 0,
});
}
}, [currIndex, props.headingOffset]);
//TODO 逻辑完善的 hash 更新
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
});
const res = [];
for (const each of items) {
const cls = `title-anchor title-level${each.level} ${
currIndex == each.index ? "active" : ""
}`;
res.push(
<div
key={each.index}
className={cls}
onClick={() => {
const el: any = document.querySelector(`[data-id="${each.text}"]`);

if (el) {
let to = el.offsetTop - props.headingOffset;
if (to <= 100) {
to = 0;
}
scroll.animateScroll.scrollTo(to);
}
}}
>
{each.text}
</div>
);
}
return (
<>
<div
className="text-center text-lg font-medium mt-4 text-gray-700 dark:text-dark cursor-pointer"
onClick={() => {
scroll.animateScroll.scrollToTop();
}}
>
目录
</div>
<div className="markdown-navigation">{res}</div>
</>
);
}
11 changes: 11 additions & 0 deletions packages/website/components/MarkdownTocBar/index.tsx
@@ -0,0 +1,11 @@
import { useMemo } from "react";
import Core from "./core";
import { parseNavStructure } from "./tools";

export default function (props: { content: string; headingOffset?: number }) {
const navData = useMemo(() => {
return parseNavStructure(props.content.replace(/`#/g, ""));
}, [props]);

return <Core items={navData} headingOffset={props.headingOffset || 0} />;
}
84 changes: 84 additions & 0 deletions packages/website/components/MarkdownTocBar/tools.ts
@@ -0,0 +1,84 @@
export interface NavItem {
index: number;
level: number;
listNo: string;
text: string;
}
export const parseNavStructure = (source: string): NavItem[] => {
const contentWithoutCode = source
.replace(/^[^#]+\n/g, "")
.replace(/(?:[^\n#]+)#+\s([^#\n]+)\n*/g, "") // 匹配行内出现 # 号的情况
.replace(/^#\s[^#\n]*\n+/, "")
.replace(/```[^`\n]*\n+[^```]+```\n+/g, "")
.replace(/`([^`\n]+)`/g, "$1")
.replace(/\*\*?([^*\n]+)\*\*?/g, "$1")
.replace(/__?([^_\n]+)__?/g, "$1")
.trim();

const pattOfTitle = /#+\s([^#\n]+)\n*/g;
const matchResult = contentWithoutCode.match(pattOfTitle);

if (!matchResult) {
return [];
}

const navData = matchResult.map((r, i) => ({
index: i,
//@ts-ignore
level: r.match(/^#+/g)[0].length,
text: r.replace(pattOfTitle, "$1"),
}));

let maxLevel = 0;
navData.forEach((t) => {
if (t.level > maxLevel) {
maxLevel = t.level;
}
});
let matchStack = [];
// 此部分重构,原有方法会出现次级标题后再次出现高级标题时,listNo重复的bug
for (let i = 0; i < navData.length; i++) {
const t: any = navData[i];
const { level } = t;
while (
matchStack.length &&
matchStack[matchStack.length - 1].level > level
) {
matchStack.pop();
}
if (matchStack.length === 0) {
const arr = new Array(maxLevel).fill(0);
arr[level - 1] += 1;
matchStack.push({
level,
arr,
});
t.listNo = trimArrZero(arr).join(".");
continue;
}
const { arr } = matchStack[matchStack.length - 1] as any;
const newArr = arr.slice();
newArr[level - 1] += 1;
matchStack.push({
level,
arr: newArr,
});
t.listNo = trimArrZero(newArr).join(".");
}
return navData as NavItem[];
};

const trimArrZero = (arr: any) => {
let start, end;
for (start = 0; start < arr.length; start++) {
if (arr[start]) {
break;
}
}
for (end = arr.length - 1; end >= 0; end--) {
if (arr[end]) {
break;
}
}
return arr.slice(start, end + 1);
};
49 changes: 25 additions & 24 deletions packages/website/components/Toc/index.tsx
@@ -1,45 +1,46 @@
import MarkdownNavbar from "markdown-navbar";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import Headroom from "headroom.js";
import scroll from "react-scroll";
import MarkdownTocBar from "../MarkdownTocBar";
export default function (props: {
content: string;
showSubMenu: "true" | "false";
}) {
const { current } = useRef({ hasInit: false });
useEffect(() => {
const el = document.querySelector("#toc-card");
if (el) {
const headroom = new Headroom(el, {
classes: {
initial: `side-bar${
props.showSubMenu == "true" ? "" : " no-submenu"
}`,
pinned: "side-bar-pinned",
unpinned: "side-bar-unpinned",
top: "side-bar-top",
notTop: "side-bar-not-top",
},
});
headroom.init();
if (!current.hasInit) {
const el = document.querySelector("#toc-card");
if (el) {
current.hasInit = true;
const headroom = new Headroom(el, {
classes: {
initial: `side-bar${
props.showSubMenu == "true" ? "" : " no-submenu"
}`,
pinned: "side-bar-pinned",
unpinned: "side-bar-unpinned",
top: "side-bar-top",
notTop: "side-bar-not-top",
},
});
headroom.init();
}
}
});
}, [current]);
return (
<div className="fixed" id="toc-card">
<div
id="toc-container"
className="bg-white w-60 card-shadow dark:card-shadow-dark ml-2 dark:bg-dark overflow-y-auto pb-2"
style={{ maxHeight: 450 }}
>
<div className="text-center text-lg font-medium mt-4 text-gray-700 dark:text-dark">
目录
</div>
<MarkdownNavbar
<MarkdownTocBar content={props.content} headingOffset={56} />
{/* <MarkdownNavbar
ordered={false}
declarative={true}
// updateHashAuto={true}
source={props.content.replace(/`#/g, "")}
headingTopOffset={56}
onHashChange={(newHash, oldHash) => {
onHashChange={(newHash: string, oldHash: string) => {
// 判断一下当前激活的元素
const el = document.querySelector(
".markdown-navigation div.active"
Expand All @@ -61,7 +62,7 @@ export default function (props: {
}
// console.log(newHash, oldHash, el);
}}
/>
/> */}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/website/package.json
Expand Up @@ -8,13 +8,13 @@
},
"dependencies": {
"@next/bundle-analyzer": "^12.2.5",
"@types/lodash": "^4.14.184",
"@types/markdown-navbar": "^1.4.0",
"@types/mermaid": "^8.2.9",
"@waline/client": "^2.6.1",
"dayjs": "^1.11.3",
"headroom.js": "^0.12.0",
"js-base64": "^3.7.2",
"markdown-navbar": "^1.4.3",
"mermaid": "^9.1.6",
"next": "latest",
"react": "18.1.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/website/pages/_app.tsx
@@ -1,7 +1,6 @@
import "../styles/globals.css";
import "markdown-navbar/dist/navbar.css";
import "../styles/side-bar.css";
import "../styles/toc-dark.css";
import "../styles/toc.css";
import "../styles/var.css";
import "../styles/github-markdown.css";
import "../styles/tip-card.css";
Expand Down
25 changes: 0 additions & 25 deletions packages/website/styles/toc-dark.css

This file was deleted.

0 comments on commit 7648c1e

Please sign in to comment.