summaryrefslogtreecommitdiffstats
path: root/src/utils/hooks/useHeadingsTree.tsx
blob: f2be40644f10a81f0e453b1153ca3444d9614b4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { Heading } from '@ts/types/app';
import { slugify } from '@utils/helpers/slugify';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useMemo, useState } from 'react';

const useHeadingsTree = (wrapper: string) => {
  const router = useRouter();
  const depths = useMemo(() => ['h2', 'h3', 'h4', 'h5', 'h6'], []);
  const [allHeadings, setAllHeadings] =
    useState<NodeListOf<HTMLHeadingElement>>();

  useEffect(() => {
    const query = depths
      .map((depth) => `${wrapper} > *:not(aside, #comments) ${depth}`)
      .join(', ');
    const result: NodeListOf<HTMLHeadingElement> =
      document.querySelectorAll(query);
    setAllHeadings(result);
  }, [depths, wrapper, router.asPath]);

  const [headingsTree, setHeadingsTree] = useState<Heading[]>([]);

  const getElementDepth = useCallback(
    (el: HTMLHeadingElement) => {
      return depths.findIndex((depth) => depth === el.localName);
    },
    [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);

      return buildTree(formattedHeadings);
    },
    [formatHeadings, buildTree]
  );

  useEffect(() => {
    if (allHeadings) {
      const headingsList = getHeadingsList(allHeadings);
      setHeadingsTree(headingsList);
    }
  }, [allHeadings, getHeadingsList]);

  return headingsTree;
};

export default useHeadingsTree;