StickyStretchableScroller.jsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import React, { useEffect, useCallback } from 'react';
  2. import PropTypes from 'prop-types';
  3. import { debounce } from 'throttle-debounce';
  4. import StickyEvents from 'sticky-events';
  5. import loggerFactory from '~/utils/logger';
  6. import { SidebarScrollerEvent } from '~/interfaces/ui';
  7. const logger = loggerFactory('growi:cli:StickyStretchableScroller');
  8. /**
  9. * USAGE:
  10. *
  11. const calcViewHeight = useCallback(() => {
  12. const containerElem = document.querySelector('#sticky-elem');
  13. const containerTop = containerElem.getBoundingClientRect().top;
  14. // stretch to the bottom of window
  15. return window.innerHeight - containerTop;
  16. });
  17. return (
  18. <StickyStretchableScroller
  19. contentsElemSelector="#long-contents-elem"
  20. stickyElemSelector="#sticky-elem"
  21. calcViewHeightFunc={calcViewHeight}
  22. >
  23. <div id="scroll-elem">
  24. ...
  25. </div>
  26. </StickyStretchableScroller>
  27. );
  28. or
  29. return (
  30. <StickyStretchableScroller
  31. scrollTargetId="scroll-elem"
  32. contentsElemSelector="#long-contents-elem"
  33. stickyElemSelector="#sticky-elem"
  34. calcViewHeightFunc={calcViewHeight}
  35. />
  36. );
  37. */
  38. const StickyStretchableScroller = (props) => {
  39. let { scrollTargetSelector } = props;
  40. const {
  41. children, contentsElemSelector, stickyElemSelector,
  42. calcViewHeightFunc, calcContentsHeightFunc,
  43. resetKey,
  44. } = props;
  45. if (scrollTargetSelector == null && children == null) {
  46. throw new Error('Either of scrollTargetSelector or children is required');
  47. }
  48. if (scrollTargetSelector == null) {
  49. scrollTargetSelector = `#${children.props.id}`;
  50. }
  51. /**
  52. * Reset scrollbar
  53. */
  54. const resetScrollbar = useCallback(() => {
  55. const contentsElem = document.querySelector(contentsElemSelector);
  56. if (contentsElem == null) {
  57. return;
  58. }
  59. const viewHeight = calcViewHeightFunc != null
  60. ? calcViewHeightFunc()
  61. : 'auto';
  62. const contentsHeight = calcContentsHeightFunc != null
  63. ? calcContentsHeightFunc(contentsElem)
  64. : contentsElem.getBoundingClientRect().height;
  65. logger.debug(`[${scrollTargetSelector}] viewHeight`, viewHeight);
  66. logger.debug(`[${scrollTargetSelector}] contentsHeight`, contentsHeight);
  67. const isScrollEnabled = viewHeight === 'auto' || (viewHeight < contentsHeight);
  68. $(scrollTargetSelector).slimScroll({
  69. color: '#666',
  70. railColor: '#999',
  71. railVisible: true,
  72. position: 'right',
  73. height: isScrollEnabled ? viewHeight : contentsHeight,
  74. wheelStep: 10,
  75. allowPageScroll: true,
  76. });
  77. // destroy
  78. if (!isScrollEnabled) {
  79. $(scrollTargetSelector).slimScroll({ destroy: true });
  80. }
  81. }, [contentsElemSelector, calcViewHeightFunc, calcContentsHeightFunc, scrollTargetSelector]);
  82. const resetScrollbarDebounced = debounce(100, resetScrollbar);
  83. useEffect(() => {
  84. document.addEventListener(SidebarScrollerEvent.RESET_SCROLLBAR, resetScrollbarDebounced);
  85. return () => {
  86. document.removeEventListener(SidebarScrollerEvent.RESET_SCROLLBAR, resetScrollbarDebounced);
  87. };
  88. }, []);
  89. const stickyChangeHandler = useCallback((event) => {
  90. logger.debug('StickyEvents.CHANGE detected');
  91. setTimeout(resetScrollbar, 100);
  92. }, [resetScrollbar]);
  93. // setup effect by sticky event
  94. useEffect(() => {
  95. if (stickyElemSelector == null) {
  96. return;
  97. }
  98. // sticky
  99. // See: https://github.com/ryanwalters/sticky-events
  100. const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector });
  101. const { stickySelector } = stickyEvents;
  102. const elem = document.querySelector(stickySelector);
  103. elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
  104. // return clean up handler
  105. return () => {
  106. elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
  107. };
  108. }, [stickyElemSelector, stickyChangeHandler]);
  109. // setup effect by resizing event
  110. useEffect(() => {
  111. const resizeHandler = (event) => {
  112. resetScrollbarDebounced();
  113. };
  114. window.addEventListener('resize', resizeHandler);
  115. // return clean up handler
  116. return () => {
  117. window.removeEventListener('resize', resizeHandler);
  118. };
  119. }, [resetScrollbarDebounced]);
  120. // setup effect on init
  121. useEffect(() => {
  122. if (resetKey != null) {
  123. resetScrollbarDebounced();
  124. }
  125. }, [resetKey, resetScrollbarDebounced]);
  126. return (
  127. <>
  128. { children }
  129. </>
  130. );
  131. };
  132. StickyStretchableScroller.propTypes = {
  133. contentsElemSelector: PropTypes.string.isRequired,
  134. children: PropTypes.node,
  135. scrollTargetSelector: PropTypes.string,
  136. stickyElemSelector: PropTypes.string,
  137. resetKey: PropTypes.any,
  138. calcViewHeightFunc: PropTypes.func,
  139. calcContentsHeightFunc: PropTypes.func,
  140. };
  141. export default StickyStretchableScroller;