@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
235 lines (234 loc) • 12.3 kB
JavaScript
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;