diff options
| author | Armand Philippot <git@armandphilippot.com> | 2021-12-21 15:05:04 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2021-12-21 15:05:04 +0100 |
| commit | ae17973618c5ad5500ae69738da222187a09b019 (patch) | |
| tree | 20edaca351c3d9b0df1b986e12255b9be7b9052d /src/utils | |
| parent | a367f605b842ad0a71a63025da15ac62ed0364a5 (diff) | |
chore: add a hook to build headings tree
Diffstat (limited to 'src/utils')
| -rw-r--r-- | src/utils/helpers/slugify.ts | 18 | ||||
| -rw-r--r-- | src/utils/hooks/useHeadingsTree.tsx | 100 |
2 files changed, 118 insertions, 0 deletions
diff --git a/src/utils/helpers/slugify.ts b/src/utils/helpers/slugify.ts new file mode 100644 index 0000000..37619bc --- /dev/null +++ b/src/utils/helpers/slugify.ts @@ -0,0 +1,18 @@ +/** + * Convert a text into a slug or id. + * https://gist.github.com/codeguy/6684588#gistcomment-3332719 + * + * @param {string} text Text to slugify. + */ +export const slugify = (text: string) => { + return text + .toString() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() + .replace(/\s+/g, '-') + .replace(/[^\w\-]+/g, '-') + .replace(/\-\-+/g, '-') + .replace(/^-|-$/g, ''); +}; diff --git a/src/utils/hooks/useHeadingsTree.tsx b/src/utils/hooks/useHeadingsTree.tsx new file mode 100644 index 0000000..8bb70c8 --- /dev/null +++ b/src/utils/hooks/useHeadingsTree.tsx @@ -0,0 +1,100 @@ +import { slugify } from '@utils/helpers/slugify'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +type Heading = { + depth: number; + id: string; + children: Heading[]; + title: string; +}; + +const useHeadingsTree = (wrapper: string) => { + const [headingsTree, setHeadingsTree] = useState<Heading[]>([]); + const depths = useMemo(() => ['h2', 'h3', 'h4', 'h5', 'h6'], []); + + const getElementDepth = useCallback( + (el: HTMLHeadingElement) => { + const elDepth = depths.findIndex((depth) => depth === el.localName); + + return elDepth; + }, + [depths] + ); + + const formatHeadings = useCallback( + (headings: NodeListOf<HTMLHeadingElement>): Heading[] => { + const formattedHeadings: Heading[] = []; + + Array.from(headings).forEach((heading) => { + const title: string = heading.textContent!; + const id = slugify(title); + const depth = getElementDepth(heading); + const children: Heading[] = []; + + heading.id = id; + + formattedHeadings.push({ + depth, + id, + children, + title, + }); + }); + + return formattedHeadings; + }, + [getElementDepth] + ); + + const buildSubTree = useCallback( + (parent: Heading, currentHeading: Heading): void => { + if (parent.depth === currentHeading.depth - 1) { + parent.children.push(currentHeading); + } else { + const lastItem = parent.children[parent.children.length - 1]; + buildSubTree(lastItem, currentHeading); + } + }, + [] + ); + + const buildTree = useCallback( + (headings: Heading[]): Heading[] => { + const tree: Heading[] = []; + + headings.forEach((heading) => { + if (heading.depth === 0) { + tree.push(heading); + } else { + const lastItem = tree[tree.length - 1]; + buildSubTree(lastItem, heading); + } + }); + + return tree; + }, + [buildSubTree] + ); + + const getHeadingsList = useCallback( + (headings: NodeListOf<HTMLHeadingElement>): Heading[] => { + const formattedHeadings = formatHeadings(headings); + const headingsList = buildTree(formattedHeadings); + + return headingsList; + }, + [formatHeadings, buildTree] + ); + + useEffect(() => { + const query = depths.map((depth) => `${wrapper} ${depth}`).join(', '); + const headings: NodeListOf<HTMLHeadingElement> = + document.querySelectorAll(query); + const headingsList = getHeadingsList(headings); + setHeadingsTree(headingsList); + }, [depths, wrapper, getHeadingsList]); + + return headingsTree; +}; + +export default useHeadingsTree; |
