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;
|