Last active
February 7, 2021 02:02
-
-
Save junhan-z/baa3ea5bd3faf77b70927151c1387278 to your computer and use it in GitHub Desktop.
Gatsby Table of Content implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react" | |
import kebabCase from "lodash/kebabCase" | |
class HeaderNode { | |
constructor(value, depth) { | |
this.value = value | |
this.depth = depth | |
this.children = [] | |
} | |
render() { | |
if (this.children.length === 0) { | |
if (this.value === undefined) { | |
// TODO: impossible, should raise issue | |
return (<></>) | |
} | |
return (<li>{this.value}</li>) | |
} | |
var childrenRender = ( | |
<ul> | |
{this.children.map(child => (child.render()))} | |
</ul> | |
) | |
if (this.value === undefined) { | |
// this node is a dummy level, wrap another level | |
return <li>{childrenRender}</li> | |
} | |
return ( | |
<li> | |
<p>{this.value}</p> | |
{childrenRender} | |
</li> | |
) | |
} | |
} | |
function _createLink(slug, heading) { | |
let anchor = kebabCase(heading.value) | |
let to = slug + "#" + anchor | |
return ( | |
<Link to={to} key={anchor}> | |
{heading.value} | |
</Link>) | |
} | |
function _createNodes(rootDepth, slug, headings) { | |
var stack = [] | |
let roots = [] | |
for (var i = 0; i < headings.length; i++) { | |
let heading = headings[i] | |
let value = _createLink(slug, heading) | |
if (heading.depth === rootDepth) { | |
let root = new HeaderNode(value, heading.depth) | |
roots.push(root) | |
stack = [root] // set stack with a fresh start | |
continue | |
} | |
// this heading is not a root (depth !== 1) | |
if (stack.length === 0) { | |
// push a dummy root node to start | |
let root = new HeaderNode(undefined, rootDepth) | |
roots.push(root) | |
stack = [root] | |
} | |
// push dummy node until its parent level | |
var depth = heading.depth | |
while (depth - 1 > stack[stack.length - 1].depth) { | |
var parent = stack[stack.length - 1] | |
var child = new HeaderNode(undefined, parent.depth + 1) | |
parent.children.push(child) | |
stack.push(child) | |
} | |
// find the parent | |
while (heading.depth - 1 !== stack[stack.length - 1].depth) { | |
stack.pop() | |
} | |
var node = new HeaderNode(value, heading.depth) | |
stack[stack.length - 1].children.push(node) | |
stack.push(node) | |
} | |
return roots | |
} | |
function createToc(slug, headings) { | |
var rootDepth = undefined | |
for (var i = 0; i < headings.length; i++) { | |
var depth = headings[i].depth | |
if (rootDepth === undefined || depth < rootDepth) { | |
rootDepth = depth | |
} | |
} | |
let nodes = _createNodes(rootDepth, slug, headings) | |
return ( | |
<ul> | |
{nodes.map(node => node.render())} | |
</ul> | |
) | |
} | |
// Feed in an array of table of contents that contains depth and value for each header | |
// this component renders a table of content the same as the raw HTML that returned from | |
// quering the `TableOfContents` fields in GraphQL in Gatsby. | |
// With the flexibilty provided, you can customize the heading element more easily | |
export default function TableOfContents({ post }) { | |
let headings = post.headings.filter(heading => heading.depth < 3) | |
if (headings.length === 0) { | |
return ( | |
<></> | |
) | |
} | |
return ( | |
<div className={`${styles.TableOfContents}`}> | |
<div><b>Table of Contents</b></div> | |
<section> | |
{createToc(post.fields.slug, headings)} | |
</section> | |
</div> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment