TOC now has active heading highlighted
This commit is contained in:
@ -1,6 +1,9 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import { List } from 'immutable';
|
import { List } from 'immutable';
|
||||||
|
import { color } from 'styles/constants';
|
||||||
import headingToSlug from '../headingToSlug';
|
import headingToSlug from '../headingToSlug';
|
||||||
import type { State, Block } from '../types';
|
import type { State, Block } from '../types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -9,44 +12,104 @@ type Props = {
|
|||||||
state: State,
|
state: State,
|
||||||
};
|
};
|
||||||
|
|
||||||
class Minimap extends Component {
|
@observer class Minimap extends Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
|
@observable activeHeading: ?string;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('scroll', this.updateActiveHeading);
|
||||||
|
this.updateActiveHeading();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('scroll', this.updateActiveHeading);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveHeading = () => {
|
||||||
|
let activeHeading = this.headingElements[0].id;
|
||||||
|
|
||||||
|
for (const element of this.headingElements) {
|
||||||
|
const bounds = element.getBoundingClientRect();
|
||||||
|
if (bounds.top <= 0) activeHeading = element.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeHeading = activeHeading;
|
||||||
|
};
|
||||||
|
|
||||||
|
get headingElements(): HTMLElement[] {
|
||||||
|
const elements = [];
|
||||||
|
const tagNames = ['h2', 'h3', 'h4', 'h5', 'h6'];
|
||||||
|
|
||||||
|
for (const tagName of tagNames) {
|
||||||
|
for (const ele of document.getElementsByTagName(tagName)) {
|
||||||
|
elements.push(ele);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
get headings(): List<Block> {
|
get headings(): List<Block> {
|
||||||
const { state } = this.props;
|
const { state } = this.props;
|
||||||
|
|
||||||
return state.document.nodes.filter((node: Block) => {
|
return state.document.nodes.filter((node: Block) => {
|
||||||
if (!node.text) return false;
|
if (!node.text) return false;
|
||||||
|
if (node.type === 'heading1') return false;
|
||||||
return node.type.match(/^heading/);
|
return node.type.match(/^heading/);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// If there are one or less headings in the document no need for a minimap
|
||||||
|
if (this.headings.size <= 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Headings>
|
<Sections>
|
||||||
{this.headings.map(heading => (
|
{this.headings.map(heading => {
|
||||||
<li>
|
const slug = headingToSlug(heading.type, heading.text);
|
||||||
<a href={`#${headingToSlug(heading.type, heading.text)}`}>
|
|
||||||
{heading.text}
|
return (
|
||||||
</a>
|
<ListItem type={heading.type}>
|
||||||
</li>
|
<Anchor href={`#${slug}`} active={this.activeHeading === slug}>
|
||||||
))}
|
{heading.text}
|
||||||
</Headings>
|
</Anchor>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Sections>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Headings = styled.ol`
|
const Anchor = styled.a`
|
||||||
margin: 0;
|
color: ${props => (props.active ? color.primary : color.slate)};
|
||||||
|
font-weight: ${props => (props.active ? 500 : 400)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Sections = styled.ol`
|
||||||
|
margin: 0 0 0 -8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 13px;
|
||||||
|
border-right: 1px solid ${color.slate};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListItem = styled.li`
|
||||||
|
position: relative;
|
||||||
|
margin-left: ${props => (props.type === 'heading2' ? '8px' : '16px')};
|
||||||
|
text-align: right;
|
||||||
|
color: ${color.slate};
|
||||||
|
padding-right: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
right: 0;
|
||||||
top: 50%;
|
top: 160px;
|
||||||
|
padding-right: 20px;
|
||||||
|
background: ${color.white};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Minimap;
|
export default Minimap;
|
||||||
|
Reference in New Issue
Block a user