TableOfContents.jsx 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import React, { useCallback, useEffect } from 'react';
  2. import PropTypes from 'prop-types';
  3. import { withTranslation } from 'react-i18next';
  4. import loggerFactory from '~/utils/logger';
  5. import PageContainer from '~/client/services/PageContainer';
  6. import NavigationContainer from '~/client/services/NavigationContainer';
  7. import { withUnstatedContainers } from './UnstatedUtils';
  8. import StickyStretchableScroller from './StickyStretchableScroller';
  9. // eslint-disable-next-line no-unused-vars
  10. const logger = loggerFactory('growi:TableOfContents');
  11. /**
  12. * @author Yuki Takei <yuki@weseek.co.jp>
  13. *
  14. */
  15. const TableOfContents = (props) => {
  16. const { t, pageContainer, navigationContainer } = props;
  17. const { pageUser } = pageContainer.state;
  18. const isUserPage = pageUser != null;
  19. const calcViewHeight = useCallback(() => {
  20. // calculate absolute top of '#revision-toc' element
  21. const parentElem = document.querySelector('.grw-side-contents-container');
  22. const parentBottom = parentElem.getBoundingClientRect().bottom;
  23. const containerElem = document.querySelector('#revision-toc');
  24. const containerTop = containerElem.getBoundingClientRect().top;
  25. const containerComputedStyle = getComputedStyle(containerElem);
  26. const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
  27. // get smaller bottom line of window height - the height of ContentLinkButtons and .system-version height) and containerTop
  28. let bottom = Math.min(window.innerHeight - 41 - 20, parentBottom);
  29. if (isUserPage) {
  30. // raise the bottom line by the height and margin-top of UserContentLinks
  31. bottom -= 45;
  32. }
  33. // bottom - revisionToc top
  34. return bottom - (containerTop + containerPaddingTop);
  35. }, [isUserPage]);
  36. const { tocHtml } = pageContainer.state;
  37. // execute after generation toc html
  38. useEffect(() => {
  39. const tocDom = document.getElementById('revision-toc-content');
  40. const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
  41. navigationContainer.addSmoothScrollEvent(anchorsInToc);
  42. }, [tocHtml, navigationContainer]);
  43. return (
  44. <StickyStretchableScroller
  45. contentsElemSelector=".revision-toc .markdownIt-TOC"
  46. stickyElemSelector=".grw-side-contents-sticky-container"
  47. calcViewHeightFunc={calcViewHeight}
  48. >
  49. { tocHtml !== ''
  50. ? (
  51. <div
  52. id="revision-toc-content"
  53. className="revision-toc-content mb-3"
  54. // eslint-disable-next-line react/no-danger
  55. dangerouslySetInnerHTML={{ __html: tocHtml }}
  56. />
  57. )
  58. : (
  59. <div
  60. id="revision-toc-content"
  61. className="revision-toc-content mb-2"
  62. >
  63. <span className="text-muted">({t('page_table_of_contents.empty')})</span>
  64. </div>
  65. ) }
  66. </StickyStretchableScroller>
  67. );
  68. };
  69. /**
  70. * Wrapper component for using unstated
  71. */
  72. const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageContainer, NavigationContainer]);
  73. TableOfContents.propTypes = {
  74. t: PropTypes.func.isRequired, // i18next
  75. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  76. navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
  77. };
  78. export default withTranslation()(TableOfContentsWrapper);