StickyStretchableScroller.jsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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 { withUnstatedContainers } from './UnstatedUtils';
  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. } = props;
  44. if (scrollTargetSelector == null && children == null) {
  45. throw new Error('Either of scrollTargetSelector or children is required');
  46. }
  47. if (scrollTargetSelector == null) {
  48. scrollTargetSelector = `#${children.props.id}`;
  49. }
  50. /**
  51. * Reset scrollbar
  52. */
  53. const resetScrollbar = useCallback(() => {
  54. const contentsElem = document.querySelector(contentsElemSelector);
  55. if (contentsElem == null) {
  56. return;
  57. }
  58. const viewHeight = calcViewHeightFunc != null
  59. ? calcViewHeightFunc()
  60. : 'auto';
  61. const contentsHeight = calcContentsHeightFunc != null
  62. ? calcContentsHeightFunc(contentsElem)
  63. : contentsElem.getBoundingClientRect().height;
  64. logger.debug(`[${scrollTargetSelector}] viewHeight`, viewHeight);
  65. logger.debug(`[${scrollTargetSelector}] contentsHeight`, contentsHeight);
  66. const isScrollEnabled = viewHeight === 'auto' || (viewHeight < contentsHeight);
  67. $(scrollTargetSelector).slimScroll({
  68. color: '#666',
  69. railColor: '#999',
  70. railVisible: true,
  71. position: 'right',
  72. height: isScrollEnabled ? viewHeight : contentsHeight,
  73. wheelStep: 10,
  74. allowPageScroll: true,
  75. });
  76. // destroy
  77. if (!isScrollEnabled) {
  78. $(scrollTargetSelector).slimScroll({ destroy: true });
  79. }
  80. }, [contentsElemSelector, calcViewHeightFunc, calcContentsHeightFunc, scrollTargetSelector]);
  81. const resetScrollbarDebounced = debounce(100, resetScrollbar);
  82. const stickyChangeHandler = useCallback((event) => {
  83. logger.debug('StickyEvents.CHANGE detected');
  84. resetScrollbar();
  85. }, [resetScrollbar]);
  86. // setup effect by sticky event
  87. useEffect(() => {
  88. if (stickyElemSelector == null) {
  89. return;
  90. }
  91. // sticky
  92. // See: https://github.com/ryanwalters/sticky-events
  93. const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector });
  94. const { stickySelector } = stickyEvents;
  95. const elem = document.querySelector(stickySelector);
  96. elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
  97. // return clean up handler
  98. return () => {
  99. elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
  100. };
  101. }, [stickyElemSelector, stickyChangeHandler]);
  102. // setup effect by resizing event
  103. useEffect(() => {
  104. const resizeHandler = (event) => {
  105. resetScrollbarDebounced();
  106. };
  107. window.addEventListener('resize', resizeHandler);
  108. // return clean up handler
  109. return () => {
  110. window.removeEventListener('resize', resizeHandler);
  111. };
  112. }, [resetScrollbarDebounced]);
  113. // setup effect by isScrollTop
  114. // useEffect(() => {
  115. // if (navigationContainer.state.isScrollTop) {
  116. // resetScrollbar();
  117. // }
  118. // }, [navigationContainer.state.isScrollTop, resetScrollbar]);
  119. // setup effect by update props
  120. useEffect(() => {
  121. resetScrollbarDebounced();
  122. }, [resetScrollbarDebounced]);
  123. return (
  124. <>
  125. { children }
  126. </>
  127. );
  128. };
  129. StickyStretchableScroller.propTypes = {
  130. contentsElemSelector: PropTypes.string.isRequired,
  131. children: PropTypes.node,
  132. scrollTargetSelector: PropTypes.string,
  133. stickyElemSelector: PropTypes.string,
  134. calcViewHeightFunc: PropTypes.func,
  135. calcContentsHeightFunc: PropTypes.func,
  136. };
  137. export default StickyStretchableScroller;