[components/toc][f]: following current h on scroll

This commit is contained in:
olayway 2022-05-24 17:50:56 +02:00
parent dba48484b9
commit 2ff44763df
4 changed files with 158 additions and 54 deletions

View File

@ -0,0 +1,25 @@
import React, { useEffect } from 'react';
export const Heading = ({ level, activeHeading, setActiveHeading }) => (props) => {
console.log(activeHeading, setActiveHeading)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveHeading(entry.target.id);
}
});
},
{ rootMargin: `0% 0% -80% 0%` }
);
observer.observe(document.getElementById(props.id));
return () => {
observer.unobserve(document.getElementById(props.id));
};
});
return React.createElement(`h${level}`, { ...props })
}

View File

@ -1,28 +1,71 @@
import Head from 'next/head' import { NextSeo } from "next-seo";
import dynamic from 'next/dynamic' import ReactPlayer from "react-player/lazy";
import { NextSeo } from 'next-seo' import LiteYouTubeEmbed from "react-lite-youtube-embed";
import siteConfig from "../config/siteConfig" import { useState, useEffect } from "react";
import LiteYouTubeEmbed from "react-lite-youtube-embed"
import { YOUTUBE_REGEX } from "../lib/constants"
const Anchor = dynamic(() => import('./Anchor').then(module => module.Anchor), { import siteConfig from "../config/siteConfig";
ssr: false import { YOUTUBE_REGEX } from "../lib/constants";
}) import getMDXComponents from "./_getMDXComponents";
import { Paragraph } from "./Paragraph";
import { Anchor } from "./Anchor";
const Paragraph = dynamic(() => import("./Paragraph").then(mod => mod.Paragraph)) // const Anchor = dynamic(() => import('./Anchor').then(module => module.Anchor), {
// ssr: false
// })
const components = { // const Paragraph = dynamic(() => import("./Paragraph").then(mod => mod.Paragraph))
Head,
p: Paragraph,
a: Anchor
}
export default function MdxPage({ children }) { // import { Toc } from './Toc'
const { Component, frontmatter: {
title, description, date, keywords, youtube, podcast, image, _raw
}} = children
let youtubeThumnbnail export default function MdxPage({ children, editUrl }) {
const [activeHeading, setActiveHeading] = useState("");
const components = getMDXComponents({
params: {
h: {
activeHeading,
setActiveHeading,
},
},
});
useEffect(() => {
if (activeHeading) {
const tocLink = document.querySelector(
`.toc-link[href="#${activeHeading}"]`
);
tocLink.classList.add("active");
// setTimeout to fix scrolling behavior
// fix switching on-off when two headings are observed
// router push
}
return () => {
if (activeHeading) {
const tocLink = document.querySelector(
`.toc-link[href="#${activeHeading}"]`
);
tocLink.classList.remove("active");
}
};
}, [activeHeading]);
const {
Component,
frontmatter: {
title,
description,
date,
keywords,
youtube,
podcast,
image,
_raw,
},
} = children;
let youtubeThumnbnail;
const youtubeId = const youtubeId =
youtube && YOUTUBE_REGEX.test(youtube) && youtube.split(/^|=|\//).pop(); youtube && YOUTUBE_REGEX.test(youtube) && youtube.split(/^|=|\//).pop();
@ -47,12 +90,14 @@ export default function MdxPage({ children }) {
const SeoTitle = title ?? titleFromUrl; const SeoTitle = title ?? titleFromUrl;
const imageUrl = image const imageUrl = image
? siteConfig.url + image ? siteConfig.url + image
: youtubeThumnbnail ? youtubeThumnbnail : null : youtubeThumnbnail
? youtubeThumnbnail
: null;
// enable editing content only for claims, concepts, and guide for now // enable editing content only for claims, concepts, and guide for now
const editUrl = ['claims', 'concepts', 'guide'].includes(_raw.sourceFileDir) const editUrl = ["claims", "concepts", "guide"].includes(_raw.sourceFileDir)
? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath ? siteConfig.repoRoot + siteConfig.repoEditPath + _raw.sourceFilePath
: null : null;
return ( return (
<> <>
@ -66,25 +111,26 @@ export default function MdxPage({ children }) {
url: `${siteConfig.url}/${_raw.flattenedPath}`, url: `${siteConfig.url}/${_raw.flattenedPath}`,
type: "article", type: "article",
article: { article: {
tags: keywords ? keywords.split(",") : [] tags: keywords ? keywords.split(",") : [],
}, },
images: imageUrl images: imageUrl
? ([ ? [
{ {
url: imageUrl, url: imageUrl,
width: 1200, width: 1200,
height: 627, height: 627,
alt: title, alt: title,
type: "image/png" type: "image/png",
}, },
]) ]
: siteConfig.nextSeo.openGraph.images, : siteConfig.nextSeo.openGraph.images,
}} }}
additionalMetaTags={[ additionalMetaTags={[
{ name: "keywords", content: keywords ? keywords : "" } { name: "keywords", content: keywords ? keywords : "" },
]} ]}
/> />
<article className="px-8 md:pl-[14rem] lg:pr-[14rem] prose max-w-none dark:prose-invert prose-a:break-all mx-auto border-2 border-yellow-500"> {/*<article className="px-8 md:pl-[14rem] lg:pr-[14rem] prose max-w-none dark:prose-invert prose-a:break-all mx-auto border-2 border-yellow-500">*/}
<article className="px-8 lg:pr-[14rem] prose max-w-none dark:prose-invert prose-a:break-all mx-auto border-2 border-yellow-500">
<header> <header>
<div className="mb-6"> <div className="mb-6">
{title && <h1 className="mb-0">{title}</h1>} {title && <h1 className="mb-0">{title}</h1>}
@ -93,17 +139,18 @@ export default function MdxPage({ children }) {
on {date} on {date}
</p> </p>
)} )}
{description && ( {description && <p className="">{description}</p>}
<p className="">{description}</p> {youtubeId && <LiteYouTubeEmbed id={youtubeId} />}
)}
{youtubeId && (
<LiteYouTubeEmbed id={youtubeId} />
)}
{podcast && ( {podcast && (
<div className="pt-4"> <div className="pt-4">
<ul className="list-disc"> <ul className="list-disc">
<li> <li>
<a className="flex items-center" target="_blank" rel="noopener" href={podcast}> <a
className="flex items-center"
target="_blank"
rel="noopener"
href={podcast}
>
<div className="w-4 mr-2"> <div className="w-4 mr-2">
<PodcastIcon /> <PodcastIcon />
</div> </div>
@ -120,28 +167,33 @@ export default function MdxPage({ children }) {
<Component components={components} /> <Component components={components} />
</div> </div>
{editUrl && ( {editUrl && (
<div className='mt-12 mb-6'> <div className="mt-12 mb-6">
<a className="flex no-underline font-semibold text-yellow-li" href={editUrl} target="_blank"> <a
className="flex no-underline font-semibold text-yellow-li"
href={editUrl}
target="_blank"
>
Edit this page Edit this page
<span className="mx-1"> <span className="mx-1">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2"> <svg
<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg> </svg>
</span> </span>
</a> </a>
</div>)} </div>
)}
</main> </main>
<div className="hidden lg:block w-[16rem] px-8 fixed top-16 right-[max(0px,50%-40rem)] bottom-0 overflow-y-auto border-2 border-blue-500">
<nav>
<ul>
<li>Heading 1</li>
<li>Heading 2</li>
<li>Heading 3</li>
<li>Heading 4</li>
<li>Heading 5</li>
</ul>
</nav>
</div>
</article> </article>
</> </>
); );

View File

@ -0,0 +1,19 @@
import Head from 'next/head'
import { Paragraph } from './Paragraph'
import { Anchor } from './Anchor'
import { Heading } from './Heading'
const getMDXComponents = ({ params: { h } }) => ({
Head,
p: Paragraph,
a: Anchor,
h1: Heading({ level: 1, ...h }),
h2: Heading({ level: 2, ...h }),
h3: Heading({ level: 3, ...h }),
h4: Heading({ level: 4, ...h }),
h5: Heading({ level: 5, ...h }),
h6: Heading({ level: 6, ...h }),
});
export default getMDXComponents;

View File

@ -11,10 +11,17 @@
"@floating-ui/react-dom-interactions": "^0.6.0", "@floating-ui/react-dom-interactions": "^0.6.0",
"@headlessui/react": "^1.4.1", "@headlessui/react": "^1.4.1",
"@heroicons/react": "^1.0.4", "@heroicons/react": "^1.0.4",
"@jsdevtools/rehype-toc": "^3.0.2",
"@mdx-js/loader": "^2.0.0",
"@mdx-js/react": "^2.0.0",
"@next/mdx": "^12.1.0",
"@silvenon/remark-smartypants": "^1.0.0",
"@tailwindcss/typography": "^0.5.2", "@tailwindcss/typography": "^0.5.2",
"contentlayer": "^0.1.2", "contentlayer": "^0.1.2",
"framer-motion": "^6.3.3", "framer-motion": "^6.3.3",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hast-util-to-string": "^2.0.0",
"mdast-util-to-hast": "^12.1.1",
"next": "^12.1.0", "next": "^12.1.0",
"next-contentlayer": "^0.1.2", "next-contentlayer": "^0.1.2",
"next-seo": "^4.28.1", "next-seo": "^4.28.1",
@ -32,6 +39,7 @@
"prettier": "^2.6.2", "prettier": "^2.6.2",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1", "rehype-slug": "^5.0.1",
"rehype-stringify": "^9.0.3",
"remark-gfm": "^3.0.0", "remark-gfm": "^3.0.0",
"remark-parse": "^10.0.1", "remark-parse": "^10.0.1",
"remark-wiki-link-plus": "^1.0.0", "remark-wiki-link-plus": "^1.0.0",