import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { debounce } from 'throttle-debounce'; import StickyEvents from 'sticky-events'; import loggerFactory from '~/utils/logger'; const logger = loggerFactory('growi:cli:StickyStretchableScroller'); /** * USAGE: * const calcViewHeight = useCallback(() => { const containerElem = document.querySelector('#sticky-elem'); const containerTop = containerElem.getBoundingClientRect().top; // stretch to the bottom of window return window.innerHeight - containerTop; }); return (
...
); or return ( ); */ const StickyStretchableScroller = (props) => { let { scrollTargetSelector } = props; const { children, contentsElemSelector, stickyElemSelector, calcViewHeightFunc, calcContentsHeightFunc, resetKey, } = props; if (scrollTargetSelector == null && children == null) { throw new Error('Either of scrollTargetSelector or children is required'); } if (scrollTargetSelector == null) { scrollTargetSelector = `#${children.props.id}`; } /** * Reset scrollbar */ const resetScrollbar = useCallback(() => { const contentsElem = document.querySelector(contentsElemSelector); if (contentsElem == null) { return; } const viewHeight = calcViewHeightFunc != null ? calcViewHeightFunc() : 'auto'; const contentsHeight = calcContentsHeightFunc != null ? calcContentsHeightFunc(contentsElem) : contentsElem.getBoundingClientRect().height; logger.debug(`[${scrollTargetSelector}] viewHeight`, viewHeight); logger.debug(`[${scrollTargetSelector}] contentsHeight`, contentsHeight); const isScrollEnabled = viewHeight === 'auto' || (viewHeight < contentsHeight); $(scrollTargetSelector).slimScroll({ color: '#666', railColor: '#999', railVisible: true, position: 'right', height: isScrollEnabled ? viewHeight : contentsHeight, wheelStep: 10, allowPageScroll: true, }); // destroy if (!isScrollEnabled) { $(scrollTargetSelector).slimScroll({ destroy: true }); } }, [contentsElemSelector, calcViewHeightFunc, calcContentsHeightFunc, scrollTargetSelector]); const resetScrollbarDebounced = debounce(100, resetScrollbar); const stickyChangeHandler = useCallback((event) => { logger.debug('StickyEvents.CHANGE detected'); setTimeout(resetScrollbar, 100); }, [resetScrollbar]); // setup effect by sticky event useEffect(() => { if (stickyElemSelector == null) { return; } // sticky // See: https://github.com/ryanwalters/sticky-events const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector }); const { stickySelector } = stickyEvents; const elem = document.querySelector(stickySelector); elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler); // return clean up handler return () => { elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler); }; }, [stickyElemSelector, stickyChangeHandler]); // setup effect by resizing event useEffect(() => { const resizeHandler = (event) => { resetScrollbarDebounced(); }; window.addEventListener('resize', resizeHandler); // return clean up handler return () => { window.removeEventListener('resize', resizeHandler); }; }, [resetScrollbarDebounced]); // setup effect on init useEffect(() => { if (resetKey != null) { resetScrollbarDebounced(); } }, [resetKey, resetScrollbarDebounced]); return ( <> { children } ); }; StickyStretchableScroller.propTypes = { contentsElemSelector: PropTypes.string.isRequired, children: PropTypes.node, scrollTargetSelector: PropTypes.string, stickyElemSelector: PropTypes.string, resetKey: PropTypes.any, calcViewHeightFunc: PropTypes.func, calcContentsHeightFunc: PropTypes.func, }; export default StickyStretchableScroller;