You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DocToc.tsx 4.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import * as classNames from 'classnames';
  21. import { debounce, memoize } from 'lodash';
  22. import * as React from 'react';
  23. import { findDOMNode } from 'react-dom';
  24. import remark from 'remark';
  25. import reactRenderer from 'remark-react';
  26. import { translate } from 'sonar-ui-common/helpers/l10n';
  27. import onlyToc from './plugins/remark-only-toc';
  28. interface Props {
  29. content: string;
  30. onAnchorClick: (href: string, event: React.MouseEvent<HTMLAnchorElement>) => void;
  31. }
  32. interface State {
  33. anchors: AnchorObject[];
  34. highlightAnchor?: string;
  35. }
  36. interface AnchorObject {
  37. href: string;
  38. title: string;
  39. }
  40. export default class DocToc extends React.PureComponent<Props, State> {
  41. debouncedScrollHandler: () => void;
  42. node: HTMLDivElement | null = null;
  43. state: State = { anchors: [] };
  44. constructor(props: Props) {
  45. super(props);
  46. this.debouncedScrollHandler = debounce(this.scrollHandler);
  47. }
  48. static getDerivedStateFromProps(props: Props) {
  49. const { content } = props;
  50. return { anchors: DocToc.getAnchors(content) };
  51. }
  52. componentDidMount() {
  53. window.addEventListener('scroll', this.debouncedScrollHandler, true);
  54. this.scrollHandler();
  55. }
  56. componentWillUnmount() {
  57. window.removeEventListener('scroll', this.debouncedScrollHandler, true);
  58. }
  59. static getAnchors = memoize((content: string) => {
  60. const file: { contents: JSX.Element } = remark()
  61. .use(reactRenderer)
  62. .use(onlyToc)
  63. .processSync('\n## doctoc\n' + content);
  64. if (file && file.contents.props.children) {
  65. let list = file.contents;
  66. let limit = 10;
  67. while (limit && list.props.children.length && list.type !== 'ul') {
  68. list = list.props.children[0];
  69. limit--;
  70. }
  71. if (list.type === 'ul' && list.props.children.length) {
  72. return list.props.children
  73. .map((li: JSX.Element | string) => {
  74. if (typeof li === 'string') {
  75. return null;
  76. }
  77. const anchor = li.props.children[0];
  78. return {
  79. href: anchor.props.href,
  80. title: anchor.props.children[0]
  81. } as AnchorObject;
  82. })
  83. .filter((item: AnchorObject | null) => item);
  84. }
  85. }
  86. return [];
  87. });
  88. scrollHandler = () => {
  89. // eslint-disable-next-line react/no-find-dom-node
  90. const node = findDOMNode(this) as HTMLElement;
  91. if (!node || !node.parentNode) {
  92. return;
  93. }
  94. const headings: NodeListOf<HTMLHeadingElement> = node.parentNode.querySelectorAll('h2[id]');
  95. const scrollTop = window.pageYOffset || document.body.scrollTop;
  96. let highlightAnchor;
  97. for (let i = 0, len = headings.length; i < len; i++) {
  98. if (headings.item(i).offsetTop > scrollTop + 120) {
  99. break;
  100. }
  101. highlightAnchor = `#${headings.item(i).id}`;
  102. }
  103. this.setState({
  104. highlightAnchor
  105. });
  106. };
  107. render() {
  108. const { anchors, highlightAnchor } = this.state;
  109. if (anchors.length === 0) {
  110. return null;
  111. }
  112. return (
  113. <div className="markdown-toc">
  114. <div className="markdown-toc-content">
  115. <h4>{translate('documentation.on_this_page')}</h4>
  116. {anchors.map(anchor => {
  117. return (
  118. <a
  119. className={classNames({ active: highlightAnchor === anchor.href })}
  120. href={anchor.href}
  121. key={anchor.title}
  122. onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
  123. this.props.onAnchorClick(anchor.href, event);
  124. }}>
  125. {anchor.title}
  126. </a>
  127. );
  128. })}
  129. </div>
  130. </div>
  131. );
  132. }
  133. }