/* * SonarQube * Copyright (C) 2009-2020 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as classNames from 'classnames'; import { debounce, memoize } from 'lodash'; import * as React from 'react'; import { findDOMNode } from 'react-dom'; import remark from 'remark'; import reactRenderer from 'remark-react'; import { translate } from 'sonar-ui-common/helpers/l10n'; import onlyToc from './plugins/remark-only-toc'; interface Props { content: string; onAnchorClick: (href: string, event: React.MouseEvent) => void; } interface State { anchors: AnchorObject[]; highlightAnchor?: string; } interface AnchorObject { href: string; title: string; } export default class DocToc extends React.PureComponent { debouncedScrollHandler: () => void; node: HTMLDivElement | null = null; state: State = { anchors: [] }; constructor(props: Props) { super(props); this.debouncedScrollHandler = debounce(this.scrollHandler); } static getDerivedStateFromProps(props: Props) { const { content } = props; return { anchors: DocToc.getAnchors(content) }; } componentDidMount() { window.addEventListener('scroll', this.debouncedScrollHandler, true); this.scrollHandler(); } componentWillUnmount() { window.removeEventListener('scroll', this.debouncedScrollHandler, true); } static getAnchors = memoize((content: string) => { const file: { contents: JSX.Element } = remark() .use(reactRenderer) .use(onlyToc) .processSync('\n## doctoc\n' + content); if (file && file.contents.props.children) { let list = file.contents; let limit = 10; while (limit && list.props.children.length && list.type !== 'ul') { list = list.props.children[0]; limit--; } if (list.type === 'ul' && list.props.children.length) { return list.props.children .map((li: JSX.Element | string) => { if (typeof li === 'string') { return null; } const anchor = li.props.children[0]; return { href: anchor.props.href, title: anchor.props.children[0] } as AnchorObject; }) .filter((item: AnchorObject | null) => item); } } return []; }); scrollHandler = () => { // eslint-disable-next-line react/no-find-dom-node const node = findDOMNode(this) as HTMLElement; if (!node || !node.parentNode) { return; } const headings: NodeListOf = node.parentNode.querySelectorAll('h2[id]'); const scrollTop = window.pageYOffset || document.body.scrollTop; let highlightAnchor; for (let i = 0, len = headings.length; i < len; i++) { if (headings.item(i).offsetTop > scrollTop + 120) { break; } highlightAnchor = `#${headings.item(i).id}`; } this.setState({ highlightAnchor }); }; render() { const { anchors, highlightAnchor } = this.state; if (anchors.length === 0) { return null; } return (

{translate('documentation.on_this_page')}

{anchors.map(anchor => { return ( ) => { this.props.onAnchorClick(anchor.href, event); }}> {anchor.title} ); })}
); } }