import PropTypes from 'prop-types' import * as React from 'react' const ViewportContext = React.createContext(undefined) // The purpose of the viewport is to create the bounding box in which the list is hosted, // and to provide the intersection observer context. export const Viewport = (props) => { const { as, ...rest } = props const viewportRef = React.useRef() const RootElement = as || 'div' const [internalState] = React.useState({ observer: undefined, observables: new Map(), scrollTop: 0, scrollBottom: 0 }) const [api] = React.useState({ observe: (element, callback, data) => { if (element) { internalState.observables.set(element, callback) if (internalState.observer) { // console.log('observing', element.textContent); internalState.observer.observe(element) } } }, unobserve: (element) => { if (element) { internalState.observables.delete(element) if (internalState.observer) { // console.log('unobserving', element.textContent); internalState.observer.unobserve(element) } } } }) React.useEffect(() => { if (viewportRef.current) { internalState.observer = new IntersectionObserver( (entries) => { return entries.forEach((entry) => { // console.log('observe change', entry.intersectionRect.height > 0, entry.target) const callback = internalState.observables.get(entry.target) if (callback) { callback({ entry, scrollTop: internalState.scrollTop, scrollBottom: internalState.scrollBottom }) } }) }, { root: viewportRef.current, rootMargin: '600px', threshold: [0, 1] } ) // Hook up to all elements which want notifications. internalState.observables.forEach((value, element) => { return internalState.observer.observe(element) }) } return () => { if (internalState.observer) { internalState.observer.disconnect() internalState.observer = undefined } } }, [internalState]) const onScroll = (ev) => { internalState.scrollTop = ev.target.scrollTop internalState.scrollBottom = ev.target.clientHeight } return ( ) } Viewport.propTypes = { as: PropTypes.string } export const _getSize = (sizeMap, defaultSize, start, end) => { const startRect = sizeMap.get(start) const endRect = sizeMap.get(end) if (startRect && endRect) { return endRect.bottom - startRect.top } return (1 + end - start) * defaultSize } export const VirtualizedListPage = (props) => { const { items, start, end, renderItem, sizeMap, defaultSize } = props const viewport = React.useContext(ViewportContext) const [isVisible, setIsVisible] = React.useState(false) const [internalState] = React.useState({ visibility: { spacer: false, first: false, last: false }, observedElements: [] }) const spacerRef = React.useRef() const firstRef = React.useRef() const lastRef = React.useRef() function evalEntry({ entry, scrollTop, scrollBottom }) { const { visibility } = internalState if (entry.target === spacerRef.current && visibility.spacer !== entry.isIntersecting) { visibility.spacer = entry.isIntersecting if (entry.isIntersecting) { setIsVisible(true) } } if (entry.target === firstRef.current && visibility.first !== entry.isIntersecting) { visibility.first = entry.isIntersecting if (!entry.isIntersecting && entry.boundingClientRect.top > scrollBottom) { // console.log('Virtualizing page', start, end) setIsVisible(false) } } if (entry.target === lastRef.current && visibility.last !== entry.isIntersecting) { visibility.last = entry.isIntersecting if (!entry.isIntersecting && entry.boundingClientRect.bottom <= 0) { // console.log('Virtualizing page', start, end) setIsVisible(false) } } } React.useEffect(() => { const { observedElements } = internalState ;[spacerRef, firstRef, lastRef].forEach((ref) => { if (ref.current) { observedElements.push(ref.current) viewport.observe(ref.current, evalEntry) } }) return () => { // console.log('unobserving', observedElements); observedElements.forEach((element) => { return viewport.unobserve(element) }) internalState.observedElements = [] } // eslint-disable-next-line react-hooks/exhaustive-deps }, [spacerRef.current, firstRef.current, lastRef.current]) return isVisible ? ( <> {items.slice(start, end + 1).map((item, index) => { const offsetIndex = index + start return renderItem({ item, index: offsetIndex, ref: offsetIndex === start ? firstRef : offsetIndex == end ? lastRef : undefined }) })} ) : (
) } VirtualizedListPage.propTypes = { defaultSize: PropTypes.any, end: PropTypes.number, items: PropTypes.shape({ slice: PropTypes.func }), renderItem: PropTypes.func, sizeMap: PropTypes.any, start: PropTypes.any } export const VirtualizedList = (props) => { const { as, items, renderItem, itemsPerPage = 20, itemSize = 40, defaultScrollIndex, reversedScroll, horizontal, ...rest } = props // console.log(defaultScrollIndex) const RootElement = props.as || 'ul' const [sizeMap] = React.useState(new Map()) const pageCount = Math.ceil(items.length / itemsPerPage) return ( {Array.from({ length: pageCount }).map((item, index) => { return })} ) } VirtualizedList.propTypes = { as: PropTypes.string, defaultScrollIndex: PropTypes.any, horizontal: PropTypes.any, itemSize: PropTypes.number, items: PropTypes.shape({ length: PropTypes.any }), itemsPerPage: PropTypes.number, renderItem: PropTypes.any, reversedScroll: PropTypes.any }