Skip to content

Commit

Permalink
Update: Rewrite Toc with tree
Browse files Browse the repository at this point in the history
  • Loading branch information
Hayao0819 committed Jul 9, 2024
1 parent d17fdea commit fb28379
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 39 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"rehype-stringify": "^10.0.0",
"sass": "^1.77.6",
"shiki": "^1.6.4",
"tailwind-children": "^0.5.0",
"tocbot": "^4.28.2",
"typescript": "5.4.5",
"usehooks-ts": "^3.1.0"
Expand Down
41 changes: 41 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/app/(hayao)/blog/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export default function BlogLayout({ children }: { children: React.ReactNode })
{tags.map((c) => {
return (
<li key={c} role="link" className="cursor-pointer p-2 text-sm hover:underline">
<Link href={`/blog/tag/${c}`}>{c}</Link>
<Link href={`/blog/tag/${c}`} className="block size-full">
{c}
</Link>
</li>
);
})}
Expand Down
6 changes: 4 additions & 2 deletions src/app/(hayao)/blog/posts/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import Breadcrumbs from "@/components/elements/Breadcrumbs";
import { BlogHeading } from "@/components/elements/Heading";
import { Link } from "@/components/elements/Link";
import { ShareCurrentURL } from "@/components/elements/ShareCurrentURL";
import Toc from "@/components/elements/Toc";
import { PostList as PostListElement } from "@/components/layouts/blog/PostPreviewList";
import Toc from "@/components/layouts/blog/Toc";
import useNoColonId from "@/hooks/useNoColonId";
import { BLOG_URL_FORMAT } from "@/lib/blog/config";
import { findPostFromUrl } from "@/lib/blog/fromurl";
Expand Down Expand Up @@ -129,7 +129,9 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
</BlogHeading>
<div className="text-center">{dateToString(postDate)}</div>
</div>
<Toc contentSelector={`#${contentId}`} className="border-y border-accent p-4" />
<div className="border-b-2 border-secondary/15">
<Toc contentSelector={`#${contentId}`} />
</div>
<div className="grow" id={contentId}>
{postData.parsed}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export const metadata: NextMetadata = genMetaData();

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html className="overflow-x-scroll md:overflow-x-auto" lang="ja">
<html className="overflow-x-scroll scroll-smooth md:overflow-x-auto" lang="ja">
<head>
<Suspense>
<GoogleAnalytics />
</Suspense>
</head>
<body className="overflow-x-hidden overscroll-y-none">
<body className=" overscroll-y-none">
<ViewTransitions>{children}</ViewTransitions>
</body>
</html>
Expand Down
115 changes: 85 additions & 30 deletions src/components/elements/Toc.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,103 @@
"use client";

import { ComponentPropsWithoutRef, useEffect, useState } from "react";
import * as tocbot from "tocbot";

import useNoColonId from "@/hooks/useNoColonId";

import Link from "./Link";
import clsx from "clsx";
import { default as NextLink } from "next/link";
import { ComponentPropsWithoutRef, memo, useEffect, useState } from "react";

interface TocProps extends ComponentPropsWithoutRef<"div"> {
contentSelector: string;
}

const Toc = ({ contentSelector, ...props }: TocProps) => {
const id = useNoColonId();
useEffect(() => {
// Tocbotの初期化
tocbot.init({
tocSelector: `#${id}`, // 目次の表示部分
contentSelector: contentSelector, // 目次を生成する対象
headingSelector: "h2, h3", // 目次に表示する見出しのタグ
});

// コンポーネントがアンマウントされたときにTocbotを破棄
return () => tocbot.destroy();
}, []);
type HeadingList = {
id: string;
text: string;
level: number;
}[];

return <div id={id} {...props}></div>;
type HeadingTree = {
id: string;
text: string;
level: number;
children: HeadingTree[];
};

export const TocWithoutTocBot = ({ contentSelector, ...props }: TocProps) => {
const [htmlIds, setHtmlIds] = useState<Element[]>([]);
useEffect(() => {
setHtmlIds(Array.from(document.querySelector(contentSelector)?.querySelectorAll("h2, h3") || []));
}, []);
const genHeadingTree = (list: HeadingList) => {
const tree: HeadingTree[] = [];
for (const item of list) {
// 木が空の場合
if (tree.length === 0) {
tree.push({ ...item, children: [] });
continue;
}

const current = tree[tree.length - 1];
// 同じレベルの場合
if (current.level === item.level) {
tree.push({ ...item, children: [] });
continue;
}

// 深い階層の場合
if (current.level < item.level) {
current.children.push({ ...item, children: [] });
continue;
}
}

return tree;
};

const elementsToHeadingList = (elements: Element[]): HeadingList =>
elements.map((e) => ({
id: e.id,
text: e.innerHTML,
level: parseInt(e.tagName.slice(1)),
}));

const elementsToHeadingTree = (elements: Element[]): HeadingTree[] => genHeadingTree(elementsToHeadingList(elements));

export const RenderHeadingTree = ({ tree }: { tree: HeadingTree[] }) => {
const levelClassNames: { [key: number]: string } = {
1: "",
2: "",
3: "ml-4",
};
return (
<div {...props}>
{htmlIds.map((e) => (
<li key={e.id}>
<Link href={`#${e.id}`}>{e.innerHTML}</Link>
<ul>
{tree.map((e) => (
<li key={e.id} className={levelClassNames[e.level]}>
<NextLink href={`#${e.id}`} scroll={true}>
{e.text}
</NextLink>
<RenderHeadingTree tree={e.children} />
</li>
))}
</ul>
);
};

export const useHeadingTree = (contentSelector: string) => {
const [tree, setTree] = useState<HeadingTree[]>([]);

useEffect(() => {
const content = document.querySelector(contentSelector);
if (!content) return;

const headingTree = elementsToHeadingTree(Array.from(content.querySelectorAll("h2, h3, h4, h5, h6")));
setTree(headingTree);
}, []);

return tree;
};

export const Toc = ({ contentSelector, ...props }: TocProps) => {
const tree = useHeadingTree(contentSelector);

return (
<div {...props} className={clsx(props.className, { hidden: tree.length < 1 })}>
<RenderHeadingTree tree={tree} />
</div>
);
};

export default Toc;
export default memo(Toc);
26 changes: 26 additions & 0 deletions src/components/elements/TocBot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ComponentPropsWithoutRef, useEffect } from "react";
import * as tocbot from "tocbot";

import useNoColonId from "@/hooks/useNoColonId";

interface TocProps extends ComponentPropsWithoutRef<"div"> {
contentSelector: string;
}
export const Tocbot = ({ contentSelector, ...props }: TocProps) => {
const id = useNoColonId();
useEffect(() => {
// Tocbotの初期化
tocbot.init({
tocSelector: `#${id}`, // 目次の表示部分
contentSelector: contentSelector, // 目次を生成する対象
headingSelector: "h2, h3", // 目次に表示する見出しのタグ
});

// コンポーネントがアンマウントされたときにTocbotを破棄
return () => tocbot.destroy();
}, []);

return <div id={id} {...props}></div>;
};

export default Tocbot;
21 changes: 21 additions & 0 deletions src/components/layouts/blog/Toc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use client";

import { clsx } from "clsx";

import { BlogHeading as Heading } from "@/components/elements/Heading";
import { RenderHeadingTree, useHeadingTree } from "@/components/elements/Toc";

const Toc = ({ contentSelector }: { contentSelector: string }) => {
const tree = useHeadingTree(contentSelector);

return (
<div className={clsx(" border-accent py-8", { hidden: tree.length < 1 })}>
<Heading level={2} className="text-accent">
お品書き
</Heading>
<RenderHeadingTree tree={tree} />
</div>
);
};

export default Toc;
5 changes: 1 addition & 4 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ module.exports = {
require("tailwindcss-textshadow"),
require("@tailwindcss/typography"),
require("tailwindcss-brand-colors"),
function ({ addVariant }) {
addVariant("child", "& > *");
addVariant("child-all", "& *");
},
require("tailwind-children"),
],
};

0 comments on commit fb28379

Please sign in to comment.