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