UNPKG

@selfcommunity/react-ui

Version:

React UI Components to integrate a Community created with SelfCommunity Platform.

235 lines (234 loc) • 12.3 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Component } from 'react'; import { throttle } from 'throttle-debounce'; import { ThresholdUnits, parseThreshold } from '../../utils/threshold'; class InfiniteScroll extends Component { constructor(props) { super(props); this.lastScrollTop = 0; this.actionTriggered = false; // variables to keep track of pull down behaviour this.startY = 0; this.currentY = 0; this.dragging = false; // will be populated in componentDidMount // based on the height of the pull down element this.maxPullDownDistance = 0; this.getScrollableTarget = () => { if (this.props.scrollableTarget instanceof HTMLElement) return this.props.scrollableTarget; if (typeof this.props.scrollableTarget === 'string') { return document.getElementById(this.props.scrollableTarget); } if (this.props.scrollableTarget === null) { console.warn(`You are trying to pass scrollableTarget but it is null. This might happen because the element may not have been added to DOM yet. See https://github.com/ankeetmaini/react-infinite-scroll-component/issues/59 for more info. `); } return null; }; this.onStart = (evt) => { if (this.lastScrollTop) return; this.dragging = true; if (evt instanceof MouseEvent) { this.startY = evt.pageY; } else if (evt instanceof TouchEvent) { this.startY = evt.touches[0].pageY; } this.currentY = this.startY; if (this._infScroll) { this._infScroll.style.willChange = 'transform'; this._infScroll.style.transition = `transform 0.2s cubic-bezier(0,0,0.31,1)`; } }; this.onMove = (evt) => { if (!this.dragging) return; if (evt instanceof MouseEvent) { this.currentY = evt.pageY; } else if (evt instanceof TouchEvent) { this.currentY = evt.touches[0].pageY; } // user is scrolling down to up if (this.currentY < this.startY) return; if (this.currentY - this.startY >= Number(this.props.pullDownToRefreshThreshold)) { this.setState({ pullToRefreshThresholdBreached: true }); } // so you can drag upto 1.5 times of the maxPullDownDistance if (this.currentY - this.startY > this.maxPullDownDistance * 1.5) return; if (this._infScroll) { this._infScroll.style.overflow = 'visible'; this._infScroll.style.transform = `translate3d(0px, ${this.currentY - this.startY}px, 0px)`; } }; this.onEnd = () => { this.startY = 0; this.currentY = 0; this.dragging = false; if (this.state.pullToRefreshThresholdBreached) { this.props.refreshFunction && this.props.refreshFunction(); this.setState({ pullToRefreshThresholdBreached: false }); } requestAnimationFrame(() => { // this._infScroll if (this._infScroll) { this._infScroll.style.overflow = 'auto'; this._infScroll.style.transform = 'none'; this._infScroll.style.willChange = 'unset'; } }); }; this.onScrollListener = (event) => { if (typeof this.props.onScroll === 'function') { // Execute this callback in next tick so that it does not affect the // functionality of the library. setTimeout(() => this.props.onScroll && this.props.onScroll(event), 0); } const target = this.props.height || this._scrollableNode ? event.target : document.documentElement.scrollTop ? document.documentElement : document.body; // return immediately if the action has already been triggered, // prevents multiple triggers. if (this.actionTriggered) return; const atBottom = this.isElementAtBottom(target, this.props.scrollThreshold); const atTop = this.isElementAtTop(target, this.props.scrollThreshold); // call the `next` function in the props to trigger the next data fetch if (atBottom && this.props.hasMoreNext) { this.actionTriggered = true; this.setState({ showLoaderNext: true }); this.props.next && this.props.next(); } // call the `next` function in the props to trigger the previous data fetch if (atTop && this.props.hasMorePrevious) { this.actionTriggered = true; this.setState({ showLoaderPrevious: true }); this.props.previous && this.props.previous(); } this.lastScrollTop = target.scrollTop; }; this.state = { showLoaderNext: false, showLoaderPrevious: false, pullToRefreshThresholdBreached: false, prevDataLength: props.dataLength }; this.throttledOnScrollListener = throttle(150, this.onScrollListener).bind(this); this.onStart = this.onStart.bind(this); this.onMove = this.onMove.bind(this); this.onEnd = this.onEnd.bind(this); } componentDidMount() { if (typeof this.props.dataLength === 'undefined') { throw new Error(`mandatory prop "dataLength" is missing. The prop is needed` + ` when loading more content. Check README.md for usage`); } this._scrollableNode = this.getScrollableTarget(); this.el = this.props.height ? this._infScroll : this._scrollableNode || window; if (this.el) { this.el.addEventListener('scroll', this.throttledOnScrollListener); } if (typeof this.props.initialScrollY === 'number' && this.el && this.el instanceof HTMLElement && this.el.scrollHeight > this.props.initialScrollY) { this.el.scrollTo(0, this.props.initialScrollY); } if (this.props.pullDownToRefresh && this.el) { this.el.addEventListener('touchstart', this.onStart); this.el.addEventListener('touchmove', this.onMove); this.el.addEventListener('touchend', this.onEnd); this.el.addEventListener('mousedown', this.onStart); this.el.addEventListener('mousemove', this.onMove); this.el.addEventListener('mouseup', this.onEnd); // get BCR of pullDown element to position it above this.maxPullDownDistance = (this._pullDown && this._pullDown.firstChild && this._pullDown.firstChild.getBoundingClientRect().height) || 0; this.forceUpdate(); if (typeof this.props.refreshFunction !== 'function') { throw new Error(`Mandatory prop "refreshFunction" missing. Pull Down To Refresh functionality will not work as expected. Check README.md for usage'`); } } } componentWillUnmount() { if (this.el) { this.el.removeEventListener('scroll', this.throttledOnScrollListener); if (this.props.pullDownToRefresh) { this.el.removeEventListener('touchstart', this.onStart); this.el.removeEventListener('touchmove', this.onMove); this.el.removeEventListener('touchend', this.onEnd); this.el.removeEventListener('mousedown', this.onStart); this.el.removeEventListener('mousemove', this.onMove); this.el.removeEventListener('mouseup', this.onEnd); } } } componentDidUpdate(prevProps) { // do nothing when dataLength is unchanged if (this.props.dataLength === prevProps.dataLength) return; this.actionTriggered = false; // update state when new data was sent in this.setState({ showLoaderPrevious: false, showLoaderNext: false }); } static getDerivedStateFromProps(nextProps, prevState) { const dataLengthChanged = nextProps.dataLength !== prevState.prevDataLength; // reset when data changes if (dataLengthChanged) { return Object.assign(Object.assign({}, prevState), { prevDataLength: nextProps.dataLength }); } return null; } isElementAtTop(target, scrollThreshold = 0.8) { const clientHeight = target === document.body || target === document.documentElement ? window.screen.availHeight : target.clientHeight; const threshold = parseThreshold(scrollThreshold); if (threshold.unit === ThresholdUnits.Pixel) { // return target.scrollTop <= threshold.value + clientHeight - target.scrollHeight + 1; return (target.scrollTop <= threshold.value && Math.abs(this.lastScrollTop - target.scrollTop) >= threshold.value / 3 && target.scrollTop < this.lastScrollTop); } else if (this.props.inverse) { return target.scrollTop <= threshold.value / 100 + clientHeight - target.scrollHeight + 1; } return target.scrollTop <= threshold.value / 100 + clientHeight && target.scrollTop < this.lastScrollTop; } isElementAtBottom(target, scrollThreshold = 0.8) { const clientHeight = target === document.body || target === document.documentElement ? window.screen.availHeight : target.clientHeight; const threshold = parseThreshold(scrollThreshold); if (threshold.unit === ThresholdUnits.Pixel) { return target.scrollTop + clientHeight >= target.scrollHeight - threshold.value; } return target.scrollTop + clientHeight >= (threshold.value / 100) * target.scrollHeight; } render() { const style = Object.assign({ height: this.props.height || 'auto', overflow: 'auto', WebkitOverflowScrolling: 'touch' }, this.props.style); const hasChildren = this.props.hasChildren || !!(this.props.children && this.props.children instanceof Array && this.props.children.length); // because heighted infiniteScroll visualy breaks // on drag down as overflow becomes visible const outerDivStyle = this.props.pullDownToRefresh && this.props.height ? { overflow: 'auto' } : {}; return (_jsx("div", Object.assign({ style: outerDivStyle, className: "infinite-scroll-component__outerdiv" }, { children: _jsxs("div", Object.assign({ className: `infinite-scroll-component ${this.props.className || ''}`, ref: (infScroll) => (this._infScroll = infScroll), style: style }, { children: [this.props.header, this.props.pullDownToRefresh && (_jsx("div", Object.assign({ style: { position: 'relative' }, ref: (pullDown) => (this._pullDown = pullDown) }, { children: _jsx("div", Object.assign({ style: { position: 'absolute', left: '50%', transform: 'translateX(-50%)', zIndex: 1 } }, { children: this.state.pullToRefreshThresholdBreached ? this.props.releaseToRefreshContent : this.props.pullDownToRefreshContent })) }))), !this.props.inverse && this.state.showLoaderPrevious && this.props.hasMorePrevious && this.props.loaderPrevious, this.props.children, this.props.inverse && this.state.showLoaderPrevious && this.props.hasMorePrevious && this.props.loaderPrevious, !this.state.showLoaderNext && !hasChildren && this.props.hasMoreNext && this.props.loaderNext, this.state.showLoaderNext && this.props.hasMoreNext && this.props.loaderNext, !this.props.hasMoreNext && this.props.endMessage, this.props.footer] })) }))); } } export default InfiniteScroll;