StickyStretchableScroller.jsx 4.3 KB

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