Skip to content

Commit 9bd62c9

Browse files
committed
feat: tocAside
1 parent 6f32a0f commit 9bd62c9

File tree

15 files changed

+496
-29
lines changed

15 files changed

+496
-29
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
PageTransition,
1010
} from './pageExtra';
1111

12+
import { LayoutRightSidePortal } from '@/providers/shared/LayoutRightSideProvider';
13+
import { ArticleRightAside } from '@/components/modules/shared/ArticleRightAside';
14+
1215
export default async function Page({ params }: { params: Record<string, any> }) {
1316
const { nid } = params;
1417
console.log('id', nid);
@@ -32,6 +35,9 @@ const PageInner = () => (
3235
</div>
3336
<IndentArticleContainer>
3437
<NoteMarkdown />
38+
<LayoutRightSidePortal>
39+
<ArticleRightAside></ArticleRightAside>
40+
</LayoutRightSidePortal>
3541
</IndentArticleContainer>
3642
</>
3743
);

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
import React, { PropsWithChildren, useEffect, useRef } from 'react';
44
import { m } from 'framer-motion';
5+
import { useShallow } from 'zustand/react/shallow';
56

67
import text from './test.md';
78

89
import { MdiClockOutline } from '@/components/icons/clock';
910
import { cn } from '@/lib/helper';
1011
import { MainMarkdown } from '@/components/ui/markdown';
11-
12-
console.log(text);
12+
import { useMainArticleStore } from '@/store/mainArticleStore';
1313

1414
export const NoteTitle = () => {
1515
const title = '我的一篇文章';
@@ -55,17 +55,24 @@ export const NoteMarkdown = () => {
5555
};
5656

5757
export const PaperLayout = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
58+
const { setOffsetHeight, setElement } = useMainArticleStore(
59+
useShallow((state) => ({
60+
setOffsetHeight: state.setOffsetHeight,
61+
setElement: state.setElement,
62+
})),
63+
);
64+
5865
const paperLayoutRef = useRef<HTMLDivElement>(null);
5966
useEffect(() => {
6067
if (!paperLayoutRef.current) return;
61-
//侧边可能需要
68+
if (setElement) setElement(paperLayoutRef.current);
6269

6370
const mainHeight = paperLayoutRef.current.offsetHeight;
64-
console.log(mainHeight);
71+
setOffsetHeight(mainHeight);
6572

6673
const ob = new ResizeObserver((entries) => {
6774
const mainHeight = (entries[0].target as HTMLElement).offsetHeight;
68-
if (mainHeight) console.log(mainHeight);
75+
if (mainHeight) setOffsetHeight(mainHeight);
6976
});
7077
ob.observe(paperLayoutRef.current);
7178

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { PropsWithChildren } from 'react';
3+
4+
import { TocAside } from '../toc/TocAside';
5+
6+
export const ArticleRightAside = ({ children }: PropsWithChildren) => {
7+
return (
8+
<aside className="sticky top-[120px] mt-[120px] h-[calc(100vh-6rem-4.5rem-150px-120px)]">
9+
<div className="relative h-full">
10+
<TocAside
11+
as="div"
12+
className="static ml-4"
13+
treeClassName="absolute h-full min-h-[120px] flex flex-col"
14+
/>
15+
</div>
16+
{!!children &&
17+
React.cloneElement(children as any, {
18+
className: 'translate-y-[calc(100%+24px)]',
19+
})}
20+
</aside>
21+
);
22+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client';
2+
3+
import type React from 'react';
4+
import { PropsWithChildren, useEffect, useMemo, useRef } from 'react';
5+
import { useShallow } from 'zustand/react/shallow';
6+
7+
import { TocTree } from './TocTree';
8+
9+
import { cn } from '@/lib/helper';
10+
import { useMainArticleStore } from '@/store/mainArticleStore';
11+
12+
export type TocAsideProps = {
13+
treeClassName?: string;
14+
accessory?: React.ReactNode | React.FC;
15+
as?: React.ElementType;
16+
className?: string;
17+
};
18+
19+
export const TocAside = ({
20+
className,
21+
children,
22+
treeClassName,
23+
as: As = 'aside',
24+
}: TocAsideProps & PropsWithChildren) => {
25+
const containerRef = useRef<HTMLUListElement>(null);
26+
const $article = useMainArticleStore(useShallow((state) => state.Element));
27+
28+
if ($article === undefined) {
29+
throw new TypeError('<Toc /> must be used in <WrappedElementProvider />');
30+
}
31+
32+
const $headings = useMemo(() => {
33+
if (!$article) {
34+
return [];
35+
}
36+
37+
return [...$article.querySelectorAll('h1,h2,h3,h4,h5,h6')].filter(($heading) => {
38+
return ($heading as HTMLElement).dataset['markdownHeading'] === 'true';
39+
}) as HTMLHeadingElement[];
40+
}, [$article]);
41+
useEffect(() => {
42+
const setMaxWidth = () => {
43+
if (containerRef.current) {
44+
containerRef.current.style.maxWidth = `${
45+
window.innerWidth - containerRef.current.getBoundingClientRect().x - 30
46+
}px`;
47+
}
48+
};
49+
50+
setMaxWidth();
51+
52+
window.addEventListener('resize', setMaxWidth);
53+
54+
return () => {
55+
window.removeEventListener('resize', setMaxWidth);
56+
};
57+
}, []);
58+
59+
return (
60+
<As className={cn('st-toc z-[3]', 'relative font-sans', className)}>
61+
<TocTree
62+
$headings={$headings}
63+
containerRef={containerRef}
64+
className={cn('absolute max-h-[75vh]', treeClassName)}
65+
/>
66+
{children}
67+
</As>
68+
);
69+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use client';
2+
3+
import type { FC, MouseEvent } from 'react';
4+
import { memo, useCallback, useMemo, useRef } from 'react';
5+
import { tv } from 'tailwind-variants';
6+
7+
import { cn } from '@/lib/helper';
8+
9+
const styles = tv({
10+
base: cn(
11+
'relative mb-[1.5px] inline-block min-w-0 max-w-full leading-normal text-neutral-content',
12+
'truncate text-left tabular-nums opacity-50 transition-all duration-500 hover:opacity-80',
13+
),
14+
variants: {
15+
status: {
16+
active: 'ml-2 opacity-100',
17+
},
18+
},
19+
});
20+
export interface ITocItem {
21+
depth: number;
22+
title: string;
23+
anchorId: string;
24+
index: number;
25+
26+
$heading: HTMLHeadingElement;
27+
}
28+
29+
export const TocItem: FC<{
30+
heading: ITocItem;
31+
32+
active: boolean;
33+
rootDepth: number;
34+
onClick?: (i: number, $el: HTMLElement | null, anchorId: string) => void;
35+
}> = memo((props) => {
36+
const { active, rootDepth, onClick, heading } = props;
37+
const { $heading, anchorId, depth, index, title } = heading;
38+
39+
const $ref = useRef<HTMLAnchorElement>(null);
40+
41+
const renderDepth = useMemo(() => {
42+
const result = depth - rootDepth;
43+
44+
return result;
45+
}, [depth, rootDepth]);
46+
47+
return (
48+
<a
49+
ref={$ref}
50+
data-index={index}
51+
href={`#${anchorId}`}
52+
className={cn(
53+
styles({
54+
status: active ? 'active' : undefined,
55+
}),
56+
)}
57+
style={useMemo(
58+
() => ({
59+
paddingLeft: depth >= rootDepth ? `${renderDepth * 0.6 + 0.5}rem` : '0.5rem',
60+
}),
61+
[depth, renderDepth, rootDepth],
62+
)}
63+
data-depth={depth}
64+
onClick={useCallback(
65+
(e: MouseEvent) => {
66+
e.preventDefault();
67+
68+
onClick?.(index, $heading, anchorId);
69+
},
70+
[onClick, index, $heading, anchorId],
71+
)}
72+
title={title}
73+
>
74+
<span className="cursor-pointer">{title}</span>
75+
</a>
76+
);
77+
});

0 commit comments

Comments
 (0)