aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils/hooks/useHeadingsTree.tsx
blob: 8bb70c882a8f79b8e0f5994a7ff3e4073f2dbe09 (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
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;