TableOfContents.jsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import loggerFactory from '@alias/logger';
  4. import { withTranslation } from 'react-i18next';
  5. import { debounce } from 'throttle-debounce';
  6. import StickyEvents from 'sticky-events';
  7. import AppContainer from '../services/AppContainer';
  8. import PageContainer from '../services/PageContainer';
  9. import { isUserPage } from '../../../lib/util/path-utils';
  10. import { createSubscribedElement } from './UnstatedUtils';
  11. const logger = loggerFactory('growi:TableOfContents');
  12. // get these value with
  13. // document.querySelector('.revision-toc').getBoundingClientRect().top
  14. const DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT = 190;
  15. const DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT_USER_PAGE = 230;
  16. const DEFAULT_REVISION_TOC_TOP_FOR_KIBELA_LAYOUT = 105;
  17. /**
  18. * @author Yuki Takei <yuki@weseek.co.jp>
  19. *
  20. * @export
  21. * @class TableOfContents
  22. * @extends {React.Component}
  23. */
  24. class TableOfContents extends React.Component {
  25. constructor(props) {
  26. super(props);
  27. this.init = this.init.bind(this);
  28. this.resetScrollbarDebounced = debounce(100, this.resetScrollbar);
  29. const { layoutType } = this.props.appContainer.config;
  30. const { path } = this.props.pageContainer.state;
  31. this.defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT;
  32. if (isUserPage(path)) {
  33. this.defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT_USER_PAGE;
  34. }
  35. if (layoutType === 'kibela') {
  36. this.defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_KIBELA_LAYOUT;
  37. }
  38. }
  39. componentDidMount() {
  40. this.init();
  41. this.resetScrollbar();
  42. }
  43. componentDidUpdate() {
  44. this.resetScrollbar();
  45. }
  46. init() {
  47. const { layoutType } = this.props.appContainer.config;
  48. if (layoutType === 'crowi') {
  49. return;
  50. }
  51. /*
  52. * set event listener
  53. */
  54. // resize
  55. window.addEventListener('resize', (event) => {
  56. this.resetScrollbarDebounced(this.defaultRevisionTocTop);
  57. });
  58. // sticky
  59. // See: https://github.com/ryanwalters/sticky-events
  60. const stickyEvents = new StickyEvents({
  61. stickySelector: '#revision-toc',
  62. });
  63. const { stickySelector } = stickyEvents;
  64. const elem = document.querySelector(stickySelector);
  65. elem.addEventListener(StickyEvents.STUCK, (event) => {
  66. logger.debug('StickyEvents.STUCK detected');
  67. this.resetScrollbar();
  68. });
  69. elem.addEventListener(StickyEvents.UNSTUCK, (event) => {
  70. logger.debug('StickyEvents.UNSTUCK detected');
  71. this.resetScrollbar(this.defaultRevisionTocTop);
  72. });
  73. }
  74. getCurrentRevisionTocTop() {
  75. // calculate absolute top of '#revision-toc' element
  76. const revisionTocElem = document.querySelector('.revision-toc');
  77. return revisionTocElem.getBoundingClientRect().top;
  78. }
  79. resetScrollbar(defaultRevisionTocTop) {
  80. const tocContentElem = document.querySelector('.revision-toc .markdownIt-TOC');
  81. if (tocContentElem == null) {
  82. return;
  83. }
  84. const revisionTocTop = defaultRevisionTocTop || this.getCurrentRevisionTocTop();
  85. // window height - revisionTocTop - .system-version height
  86. const viewHeight = window.innerHeight - revisionTocTop - 20;
  87. const tocContentHeight = tocContentElem.getBoundingClientRect().height + 15; // add margin
  88. logger.debug('viewHeight', viewHeight);
  89. logger.debug('tocContentHeight', tocContentHeight);
  90. if (viewHeight < tocContentHeight) {
  91. $('#revision-toc-content').slimScroll({
  92. railVisible: true,
  93. position: 'right',
  94. height: viewHeight,
  95. });
  96. }
  97. else {
  98. $('#revision-toc-content').slimScroll({ destroy: true });
  99. }
  100. }
  101. render() {
  102. const { tocHtml } = this.props.pageContainer.state;
  103. return (
  104. <div
  105. id="revision-toc-content"
  106. className="revision-toc-content"
  107. // eslint-disable-next-line react/no-danger
  108. dangerouslySetInnerHTML={{
  109. __html: tocHtml,
  110. }}
  111. />
  112. );
  113. }
  114. }
  115. /**
  116. * Wrapper component for using unstated
  117. */
  118. const TableOfContentsWrapper = (props) => {
  119. return createSubscribedElement(TableOfContents, props, [AppContainer, PageContainer]);
  120. };
  121. TableOfContents.propTypes = {
  122. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  123. pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
  124. };
  125. export default withTranslation()(TableOfContentsWrapper);