Skip to content

Commit 232859c

Browse files
committed
feat: 拓展md header
1 parent d7e9bf8 commit 232859c

File tree

5 files changed

+133
-1
lines changed

5 files changed

+133
-1
lines changed

src/app/(app)/notes/[nid]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const PageTransition = ({ children }: PropsWithChildren) => {
2929
opacity: 1,
3030
transition: { duration: 0.5, type: 'spring', damping: 20, stiffness: 200 },
3131
}}
32+
className=" min-w-0"
3233
>
3334
{children}
3435
</m.div>

src/app/(app)/notes/[nid]/test.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# 结构
44

55
整个博客,不仅仅由 yyblog 组成,还有 ybg-cli 脚手架,用于自动创建删除文章以及编译文章。
6+
![Minion](https://octodex.github.com/images/minion.png)
67

78
- **yyblog**
89

@@ -20,6 +21,12 @@
2021

2122
采用`Nextjs`+`Typescript`+`Tailwindw`为主要技术。
2223

24+
<details>
25+
<summary>点击展开更多信息</summary>
26+
这是详细的内容,可以在点击时展开。
27+
</details>
28+
29+
2330
效仿 Hexo 采用纯前端,文章编写删除编译都在本地运行,对前端工程师更友好。
2431
Nextjs 同时也支持全栈开发。简单的 sql 语句也能够对文章进行增删改查。
2532

src/components/ui/markdown/Markdown.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { FC, PropsWithChildren } from 'react';
99
import { memo, Suspense, useMemo, useRef } from 'react';
1010

1111
import { MParagraph } from './renderbers/paragraph';
12+
import { MHeader } from './renderbers/heading';
1213

1314
export interface MdProps {
1415
value?: string;
@@ -59,11 +60,19 @@ export const Markdown: FC<MdProps & MarkdownToJSX.Options & PropsWithChildren &
5960
...overrides,
6061
},
6162
extendsRules: {
63+
heading: {
64+
react(node, output, state) {
65+
return (
66+
<MHeader id={node.id} level={node.level} key={state?.key}>
67+
{output(node.content, state!)}
68+
</MHeader>
69+
);
70+
},
71+
},
6272
...extendsRules,
6373
...renderers,
6474
},
6575
additionalParserRules: {
66-
// Additional parser rules can be defined here
6776
...additionalParserRules,
6877
},
6978
...rest,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { DOMAttributes } from 'react';
2+
import { createElement, useId } from 'react';
3+
4+
import useIsClient from '@/hooks/common/use-is-client';
5+
import { springScrollToElement } from '@/lib/scroller';
6+
7+
interface HeadingProps {
8+
id: string;
9+
className?: string;
10+
children: React.ReactNode;
11+
level: number;
12+
}
13+
14+
export const MHeader = (props: HeadingProps) => {
15+
const { children, id, level } = props;
16+
17+
const rid = useId();
18+
19+
const isClient = useIsClient();
20+
21+
const nextId = `${rid}${id}`;
22+
23+
return createElement<DOMAttributes<HTMLHeadingElement>, HTMLHeadingElement>(
24+
`h${level}`,
25+
{
26+
id: nextId,
27+
className: 'group flex items-center',
28+
29+
'data-markdown-heading': true,
30+
} as any,
31+
null,
32+
<>
33+
<span>{children}</span>
34+
{isClient && (
35+
<span
36+
className="center ml-2 inline-flex cursor-pointer select-none text-accent opacity-0 transition-opacity duration-200 group-hover:opacity-100"
37+
role="button"
38+
tabIndex={0}
39+
onClick={() => {
40+
const { state } = history;
41+
history.replaceState(state, '', `#${nextId}`);
42+
springScrollToElement(document.getElementById(nextId)!, -100);
43+
}}
44+
>
45+
<i className="i-mingcute-hashtag-line" />
46+
</span>
47+
)}
48+
</>,
49+
);
50+
};

src/lib/scroller.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client';
2+
3+
import type { Spring } from 'framer-motion';
4+
import { animateValue } from 'framer-motion';
5+
6+
const spring: Spring = {
7+
type: 'spring',
8+
stiffness: 1000,
9+
damping: 250,
10+
};
11+
12+
export const springScrollTo = (y: number) => {
13+
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
14+
15+
const stopSpringScrollHandler = () => {
16+
animation.stop();
17+
};
18+
const animation = animateValue({
19+
keyframes: [scrollTop + 1, y],
20+
autoplay: true,
21+
...spring,
22+
onPlay() {
23+
window.addEventListener('wheel', stopSpringScrollHandler);
24+
window.addEventListener('touchmove', stopSpringScrollHandler);
25+
},
26+
27+
onUpdate(latest) {
28+
if (latest <= 0) {
29+
animation.stop();
30+
}
31+
32+
window.scrollTo(0, latest);
33+
},
34+
});
35+
36+
animation.then(() => {
37+
window.removeEventListener('wheel', stopSpringScrollHandler);
38+
window.removeEventListener('touchmove', stopSpringScrollHandler);
39+
});
40+
41+
return animation;
42+
};
43+
44+
export const springScrollToTop = () => {
45+
return springScrollTo(0);
46+
};
47+
48+
export const springScrollToElement = (element: HTMLElement, delta = 40) => {
49+
const y = calculateElementTop(element);
50+
51+
const to = y + delta;
52+
53+
return springScrollTo(to);
54+
};
55+
56+
const calculateElementTop = (el: HTMLElement) => {
57+
let top = 0;
58+
59+
while (el) {
60+
top += el.offsetTop;
61+
el = el.offsetParent as HTMLElement;
62+
}
63+
64+
return top;
65+
};

0 commit comments

Comments
 (0)