Skip to content

Commit

Permalink
Merge pull request #37 from Pitayan/33-table-of-contents
Browse files Browse the repository at this point in the history
33 table of contents
  • Loading branch information
daiyanze authored Jan 15, 2023
2 parents 5e2017e + ef79327 commit 55e7b8e
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 24 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Use the plugin options to tune up your blog.
| ------------------------ | :------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- |
| siteAssets | src/assets | The static assets for the site. e.g. Logo / Cover image |
| postsPerPage | 10 | How many posts to be displayed in each list page |
| tableOfContentsLevels | 2 | The maximum levels of table-of-contents to display |
| mailChimpEndpoint | "null" | The embeded form endpoint for your MailChimp account |
| mailChimpTimeout | 3500 | The AP request timeout for the MailChimp subscription |
| applyGatsbyRemarkPlugins | () => defaultGatsbyRemarkPlugins | Return your gatsby-plugin-remark plugins via this function. The argument of this function is the built-in plugins settings. |
Expand All @@ -152,6 +153,7 @@ Example
options: {
siteAssets: "src/assets",
postsPerPage: 10,
tableOfContentsLevels: 3,
mailChimpEndpoint: "***",
mailChimpTimeout: 3500,
applyGatsbyRemarkPlugins(defaultGatsbyRemarkPlugins) => {
Expand Down Expand Up @@ -409,11 +411,11 @@ This example site has integrated the following plugins:

This theme introduces some custom events to allow 3rd party scripts (or your own scripts) to follow up the on-page behaviors.

| Item | Custom Event Detail Property | Description |
| --------------------------- | :-------------------------------------------------- | :-------------------------------------------------------- |
| CUSTOM_EVENT_SUBSCRIPTION | { email: string } | Occurs when a user successfully subscribed to newsletters |
| CUSTOM_EVENT_TOGGLE_THEME | { theme: 'dark' | 'light' } | Occurs when a user changes the theme dark <-> light |
| CUSTOM_EVENT_SOCIAL_SHARING | { sns: 'Twitter' | 'Facebook' | 'Pocket' | 'copy' } | Occurs when a user shares to SNS / copes the post URL |
| Item | Custom Event Detail Property | Description |
| --------------------------- | :----------------------------------------------------- | :-------------------------------------------------------- |
| CUSTOM_EVENT_SUBSCRIPTION | { email: string } | Occurs when a user successfully subscribed to newsletters |
| CUSTOM_EVENT_TOGGLE_THEME | { theme: 'dark' \| light' } | Occurs when a user changes the theme dark <-> light |
| CUSTOM_EVENT_SOCIAL_SHARING | { sns: 'Twitter' \| 'Facebook' \| 'Pocket' \| 'copy' } | Occurs when a user shares to SNS / copes the post URL |


## Browser Support
Expand Down
8 changes: 8 additions & 0 deletions packages/gatsby-theme-pitayan/src/assets/css/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@
@apply font-semibold;
@apply opacity-60;
@apply hover:opacity-100;
@apply active:opacity-100;
@apply transition-all;
@apply duration-150;
@apply ease-in-out;

&.active {
@apply opacity-100;
@apply dark:text-white;
}

/* Dark Mode */
@apply dark:hover:opacity-100;
@apply dark:active:opacity-100;
@apply dark:hover:text-white;
@apply dark:active:text-white;
}

.avatar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const PostMeta: React.FC<PostMetaProps> = ({
}) => {
return (
<span className={`text-gray-500 ${className}`}>
{date}{timeToRead} min read
{date}{Math.round(timeToRead)} min read
</span>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useLayoutEffect, useRef, useState } from "react"
import React, { memo, useLayoutEffect, useRef, useState, forwardRef } from "react"
import { useTextSelection } from "@pitayan/gatsby-theme-pitayan/src/hooks"
import { CUSTOM_EVENT_SOCIAL_SHARING } from "@pitayan/gatsby-theme-pitayan/src/constants"
import {
Expand All @@ -10,14 +10,12 @@ import { FiCopy } from "react-icons/fi"
import { SiTwitter } from "react-icons/si"

type PopupContentProps = {
target: HTMLElement
}

type SelectionPopupProps = {
target: HTMLElement
}

const PopupContent: React.FC<PopupContentProps> = ({ target }) => {
const PopupContent: React.FC<PopupContentProps> = forwardRef((_, containerRef) => {
const ref = useRef(null)
const [offsetWidth, setOffsetWidth] = useState<number>(0)
const [offsetHeight, setOffsetHeight] = useState<number>(0)
Expand All @@ -35,7 +33,7 @@ const PopupContent: React.FC<PopupContentProps> = ({ target }) => {
}, [ref])

const { left, top, textContent } = useTextSelection(
target,
containerRef.current || document.body,
// half size of the full popup
offsetWidth / 2,
// default height + :after (the triangle bottom) + :after_padding
Expand Down Expand Up @@ -72,14 +70,14 @@ const PopupContent: React.FC<PopupContentProps> = ({ target }) => {
</button>
</div>
)
}
})

const SelectionPopup: React.FC<SelectionPopupProps> = ({ target }) => {
const SelectionPopup: React.FC<SelectionPopupProps> = (_, containerRef) => {
return (
<Portal>
<PopupContent target={target} />
<PopupContent ref={containerRef} />
</Portal>
)
}

export default memo(SelectionPopup)
export default memo(forwardRef(SelectionPopup))
101 changes: 101 additions & 0 deletions packages/gatsby-theme-pitayan/src/components/TableOfContents/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { memo, useRef, useLayoutEffect, forwardRef } from "react"

const Content: React.FC<any> = forwardRef(({
items,
levels,
lvlRef,
}, ref) => {
if (lvlRef.current < levels) {
lvlRef.current++
} else {
return <></>
}

return (
<ul ref={ref} className="list-none">
{items.map((data, index) => {
return (
<li className="mt-2" key={index}>
<a className="site-link" href={data.url}>{data.title}</a>
{data.items && <Content items={data.items} levels={levels} lvlRef={lvlRef} />}
</li>
)
})}
</ul>
)
})

// NOTE: Currently by default, table-of-contents displays 1 level of headings H1 considering performance
const TableOfContents: React.FC<any> = ({
className = "",
title = "Table of Contents",
levels = 2,
items,
}, articleRef) => {
const lvlRef = useRef(0)
const listRef = useRef(null)
const nodesMap = new Map<HTMLElement, {
prev: HTMLElement,
next: HTMLElement,
bottom?: number
}>()

useLayoutEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
const listHeadingNode = nodesMap.get(entry.target)
if (entry.intersectionRatio <= 0) {
if (entry.boundingClientRect.bottom <= 0) {
listHeadingNode.bottom = entry.boundingClientRect.bottom
listHeadingNode.prev.classList.remove('active')
listHeadingNode.next.classList.add('active')
}
}

if (entry.intersectionRatio > 0) {
if (entry.boundingClientRect.bottom > 0 && listHeadingNode.bottom < 0) {
listHeadingNode.bottom = entry.boundingClientRect.bottom
listHeadingNode.next.classList.remove('active')
listHeadingNode.prev.classList.add('active')
}
}
})
})

if (listRef.current) {
const tocHeadingNodes = listRef.current.querySelectorAll(`a[href]`)

if (!tocHeadingNodes) {
return
}

tocHeadingNodes.forEach((node, idx) => {
const url = node.getAttribute('href')
const articleAnchorNode = articleRef.current.querySelector(`a[href="${url}"]`)

if (!articleAnchorNode) {
return
}

const previousElementToHeading = articleAnchorNode.parentElement.previousElementSibling ?? articleAnchorNode.parentElement;

nodesMap.set(previousElementToHeading, {
prev: tocHeadingNodes[Math.max(idx - 1, 0)],
next: tocHeadingNodes[idx],
})

observer.observe(previousElementToHeading)

})
}
}, [])

return (
<div className={`table-of-contents ${className}`}>
<h5>{title}</h5>
<Content ref={listRef} items={items} levels={levels} lvlRef={lvlRef} />
</div>
)
}

export default memo(forwardRef(TableOfContents))
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const projectRoot = path.resolve(__dirname, "../../../")

module.exports = async function createPages(
{ graphql, actions },
{ postsPerPage = 10 }
{ postsPerPage = 10, tableOfContentsLevels = 2 }
) {
const { createPage } = actions

Expand Down Expand Up @@ -98,6 +98,7 @@ module.exports = async function createPages(
context: {
// We can use the values in this context in our page layout component.
slug: node.fields.slug,
tableOfContentsLevels,
},
})
})
Expand All @@ -114,6 +115,7 @@ module.exports = async function createPages(
component: `${component}?__contentFilePath=${node.internal.contentFilePath}`,
context: {
slug: node.fields.slug,
tableOfContentsLevels,
previous: {
title: previousPost.frontmatter.title,
slug: previousPost.fields.slug,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ module.exports = function pluginOptionsSchema({ Joi }) {
.integer()
.greater(0)
.description("How many posts to be displayed in each list page"),
tableOfContentsLevels: Joi.number()
.integer()
.min(1)
.max(6)
.description("The maximum levels of table-of-contents to display"),
mailChimpEndpoint: Joi.string()
.uri()
.description("The embeded form endpoint of your MailChimp account"),
Expand Down
45 changes: 37 additions & 8 deletions packages/gatsby-theme-pitayan/src/templates/post/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { useState, useRef } from "react"
import { graphql, Link } from "gatsby"
import { MDXProvider } from "@mdx-js/react"
import { RiArrowLeftLine, RiArrowRightLine } from "react-icons/ri"
Expand All @@ -14,6 +14,7 @@ import BackToTop from "@pitayan/gatsby-theme-pitayan/src/components/BackToTop"
import PostAuthors from "@pitayan/gatsby-theme-pitayan/src/components/PostAuthors"
import SelectionPopup from "@pitayan/gatsby-theme-pitayan/src/components/SelectionPopup"
import ScrollVisibility from "@pitayan/gatsby-theme-pitayan/src/components/ScrollVisibility"
import TableOfContents from "@pitayan/gatsby-theme-pitayan/src/components/TableOfContents"

import { useSiteMetadata } from "@pitayan/gatsby-theme-pitayan/src/hooks"
import { SOCIAL_RESOURCES } from "@pitayan/gatsby-theme-pitayan/src/constants"
Expand All @@ -24,6 +25,9 @@ type PostProps = {
data: {
mdx: {
body: string
tableOfContents: {
items: {url: string; title: string}[]
}
frontmatter: {
author: Author[]
title: string
Expand Down Expand Up @@ -64,6 +68,7 @@ const Post: React.FC<PostProps> = ({
data: {
mdx: {
body,
tableOfContents,
frontmatter: {
author: coAuthors,
title,
Expand All @@ -79,16 +84,19 @@ const Post: React.FC<PostProps> = ({
},
},
pageContext: {
tableOfContentsLevels,
previous,
next
},
children,
}) => {
const [postTarget, setPostTarget] = useState<HTMLElement | null>()
const articleRef = useRef<HTMLElement | null>(null)
const postImage = getImage(hero?.medium)
const { siteUrl } = useSiteMetadata()
const { href: url } = useLocation()

console.log('levels', tableOfContentsLevels)

const authors = coAuthors.map(({ id, yamlId, name, bio, sns }) => {
const socialUrls = sns
.filter((s: string[]) => s[0] != "mailto" && s[0] != "url")
Expand Down Expand Up @@ -119,7 +127,7 @@ const Post: React.FC<PostProps> = ({
timeToRead={timeToRead}
authors={authors}
>
<SelectionPopup target={postTarget} />
<SelectionPopup ref={articleRef} />

<div className="hidden md:block">
<ScrollVisibility className="fixed right-[6%] bottom-[6%] flex flex-col justify-center z-50">
Expand Down Expand Up @@ -152,11 +160,31 @@ const Post: React.FC<PostProps> = ({
<PostImage image={postImage} />
</div>

<article className="markdown" ref={ref => setPostTarget(ref)}>
<MDXProvider components={{}}>
{children}
</MDXProvider>
</article>
<div className="lg:grid lg:grid-cols-9 lg:gaps-5">
<SocialSharing
url={url}
title={title}
hashtags={categories.join(",")}
description={description}
className="hidden lg:flex text-xl mt-12 flex-col lg:col-start-1 lg:col-end-3 top-[2rem] sticky self-start max-h-full overflow-y-auto space-y-6"
twitter
facebook
linkedin
pocket
copy
/>
<article className="markdown lg:col-start-3 lg:col-end-8" ref={articleRef}>
<MDXProvider components={{}}>
{children}
</MDXProvider>
</article>
<TableOfContents
className="hidden lg:block lg:col-span-2 mt-12 top-[2rem] sticky self-start text-xs font-semibold mx-auto mb-6 overflow-y-auto max-h-full"
items={tableOfContents.items}
ref={articleRef}
levels={tableOfContentsLevels}
/>
</div>

<div className="my-8 max-w-lg md:max-w-2xl mx-auto">
<div className="block sm:flex flex-wrap items-center justify-center sm:justify-between">
Expand Down Expand Up @@ -227,6 +255,7 @@ export const pageQuery = graphql`
mdx(fields: { slug: { eq: $slug } }) {
body
timeToRead
tableOfContents
frontmatter {
author {
id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pharetra egesta

Donec venenatis blandit erat. In a sodales neque, sit amet dignissim eros. Donec sit amet semper ligula. Praesent vel ligula ultrices, efficitur nisi non, tincidunt dolor. Proin a dolor dolor. Fusce malesuada ornare felis eu auctor. Morbi vestibulum venenatis lectus scelerisque tempus.

## Firstly level 1

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pharetra egestas odio id accumsan. In vel ipsum quam. Fusce scelerisque rutrum tempus. Mauris in velit sapien. In mi nibh, dignissim in ligula ut, faucibus lacinia libero. Etiam volutpat turpis est, a scelerisque sem commodo eu. Morbi lectus quam, molestie eget vestibulum in, porta ac tellus. Aliquam feugiat, orci non vehicula tristique, augue ex congue dui, non suscipit velit purus vitae diam. Phasellus dui augue, faucibus sed neque id, pellentesque porta magna. Cras nec lacinia dui. Pellentesque lacinia, nisi non porta condimentum, risus nisi semper erat, vel ullamcorper neque justo sit amet erat. Mauris lacinia dolor in arcu tempor pretium.

### Firstly level 2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pharetra egestas odio id accumsan. In vel ipsum quam. Fusce scelerisque rutrum tempus. Mauris in velit sapien. In mi nibh, dignissim in ligula ut, faucibus lacinia libero. Etiam volutpat turpis est, a scelerisque sem commodo eu. Morbi lectus quam, molestie eget vestibulum in, porta ac tellus. Aliquam feugiat, orci non vehicula tristique, augue ex congue dui, non suscipit velit purus vitae diam. Phasellus dui augue, faucibus sed neque id, pellentesque porta magna. Cras nec lacinia dui. Pellentesque lacinia, nisi non porta condimentum, risus nisi semper erat, vel ullamcorper neque justo sit amet erat. Mauris lacinia dolor in arcu tempor pretium.

#### Firstly level 3

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pharetra egestas odio id accumsan. In vel ipsum quam. Fusce scelerisque rutrum tempus. Mauris in velit sapien. In mi nibh, dignissim in ligula ut, faucibus lacinia libero. Etiam volutpat turpis est, a scelerisque sem commodo eu. Morbi lectus quam, molestie eget vestibulum in, porta ac tellus. Aliquam feugiat, orci non vehicula tristique, augue ex congue dui, non suscipit velit purus vitae diam. Phasellus dui augue, faucibus sed neque id, pellentesque porta magna. Cras nec lacinia dui. Pellentesque lacinia, nisi non porta condimentum, risus nisi semper erat, vel ullamcorper neque justo sit amet erat. Mauris lacinia dolor in arcu tempor pretium.

# Secondly

Vivamus hendrerit eleifend nunc sed imperdiet. Ut sed sem at nisi rhoncus faucibus. Cras varius ex quis semper lacinia. Sed eu leo lectus. Quisque pellentesque condimentum metus, non dapibus justo lobortis mattis. Suspendisse sit amet eros justo. Cras iaculis elementum nisl, quis vehicula diam tincidunt quis. Aliquam id aliquam odio, bibendum tristique nibh. Suspendisse mattis pharetra turpis, et pharetra urna eleifend quis. Proin tempor molestie magna in egestas. Vivamus arcu ante, eleifend eget mattis ac, aliquam at lorem. Nulla vehicula consectetur pulvinar. Quisque sit amet ligula ac ipsum eleifend ultrices. Suspendisse potenti. Aenean a massa iaculis, posuere elit in, vehicula arcu. Aliquam erat volutpat.
Expand Down
1 change: 1 addition & 0 deletions packages/www/gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ module.exports = {
options: {
siteAssets: "src/assets",
postsPerPage: 6,
tableOfContentsLevels: 3,
mailChimpEndpoint:
"https://pitayanblog.us14.list-manage.com/subscribe/post?u=234bf6777b76872feb7d92a68&amp;id=27fad95f3b",
mailChimpTimeout: 3500,
Expand Down

0 comments on commit 55e7b8e

Please sign in to comment.