-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPageNav.tsx
102 lines (81 loc) · 2.75 KB
/
PageNav.tsx
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
import { computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Component } from 'react';
import { Nav, NavProps } from 'react-bootstrap';
import { scrollTo, uniqueID } from 'web-utility';
const HeadingSelector =
Array.from(new Array(6), (_, index) => `h${++index}`) + '';
type HeadingMeta = Record<'id' | 'text', string> &
Record<'level' | 'top', number>;
export interface PageNavProps extends NavProps {
depth?: number;
onItemClick?: (item: HeadingMeta) => any;
}
@observer
export class PageNav extends Component<PageNavProps> {
static displayName = 'PageNav';
@observable
accessor list: HeadingMeta[] = [];
@observable
accessor scrollY = 0;
@computed
get currentActiveId() {
const { list, scrollY } = this;
const index = list.findIndex(({ top }) => top > scrollY);
return list[index - 1]?.id;
}
updateScrollY = () => (this.scrollY = window.scrollY);
componentDidMount() {
if (!globalThis.document) return;
window.addEventListener('scroll', this.updateScrollY);
this.list = Array.from(
document.querySelectorAll<HTMLHeadingElement>(HeadingSelector),
element => {
element.id ||= uniqueID();
const { id, tagName, textContent, offsetTop } = element;
return {
id,
level: +tagName[1],
text: textContent.trim(),
top: offsetTop
};
}
);
}
componentWillUnmount(): void {
globalThis.removeEventListener?.('scroll', this.updateScrollY);
}
scrollTo = (meta: HeadingMeta) => () => {
setTimeout(() => scrollTo(`[id="${meta.id}"]`));
this.props.onItemClick?.(meta);
};
renderLink = ({ id, level, text, top }: HeadingMeta) => {
const { currentActiveId } = this,
{ depth = Infinity } = this.props;
return (
level <= depth && (
<Nav.Item key={id} style={{ textIndent: `${level - 1}rem` }}>
<Nav.Link
href={`#${id}`}
active={currentActiveId === id}
onClick={this.scrollTo({ id, level, text, top })}
>
{text}
</Nav.Link>
</Nav.Item>
)
);
};
render() {
const { list } = this,
{ style, variant = 'underline', ...props } = this.props;
return (
<Nav
{...{ ...props, variant }}
style={{ flexDirection: 'column', ...style }}
>
{list.map(this.renderLink)}
</Nav>
);
}
}