Skip to content

Instantly share code, notes, and snippets.

@xettri
Created November 26, 2020 18:56
Show Gist options
  • Save xettri/cfb3a9b8982f91717f4b2e2a12199a83 to your computer and use it in GitHub Desktop.
Save xettri/cfb3a9b8982f91717f4b2e2a12199a83 to your computer and use it in GitHub Desktop.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const throttle = (delay, noTrailing, callback, debounceMode) => {
let timeoutID;
let cancelled = false;
let lastExec = 0;
function clearExistingTimeout() {
if (timeoutID) {
clearTimeout(timeoutID);
}
}
function cancel() {
clearExistingTimeout();
cancelled = true;
}
if (typeof noTrailing !== 'boolean') {
debounceMode = callback;
callback = noTrailing;
noTrailing = undefined;
}
function wrapper(...arguments_) {
let self = this;
let elapsed = Date.now() - lastExec;
if (cancelled) {
return;
}
function exec() {
lastExec = Date.now();
callback.apply(self, arguments_);
}
function clear() {
timeoutID = undefined;
}
if (debounceMode && !timeoutID) {
exec();
}
clearExistingTimeout();
if (debounceMode === undefined && elapsed > delay) {
exec();
} else if (noTrailing !== true) {
timeoutID = setTimeout(
debounceMode ? clear : exec,
debounceMode === undefined ? delay - elapsed : delay,
);
}
}
wrapper.cancel = cancel;
return wrapper;
};
class InfiniteScrollComponent extends Component {
constructor(props) {
super(props);
this.scrollListener = throttle(150, this.onScrollListener).bind(this);
this.eventListenerOptions = this.eventListenerOptions.bind(this);
this.mousewheelListener = this.mousewheelListener.bind(this);
}
/* true if action triggered */
actionTriggered = false;
componentDidMount() {
this.options = this.eventListenerOptions();
this.attachScrollListener();
}
componentDidUpdate() {
this.attachScrollListener();
}
componentWillUnmount() {
this.detachScrollListener();
this.detachMousewheelListener();
}
UNSAFE_componentWillReceiveProps(props) {
// do nothing when dataLength is unchanged
const { dataLength } = this.props;
if (dataLength === props.dataLength) return;
this.actionTriggered = false;
}
// check passive support for browser
isPassiveSupported() {
let passive = false;
const testOptions = {
// eslint-disable-next-line getter-return
get passive() {
passive = true;
},
};
try {
document.addEventListener('test', null, testOptions);
document.removeEventListener('test', null, testOptions);
} catch (e) {}
return passive;
}
eventListenerOptions() {
let options = this.props.useCapture;
if (this.isPassiveSupported()) {
options = {
useCapture: this.props.useCapture,
passive: true,
};
} else {
options = { passive: false };
}
return options;
}
detachMousewheelListener() {
let scrollEl = window;
if (this.props.useWindow === false) {
scrollEl = this.scrollComponent.parentNode;
}
scrollEl.removeEventListener(
'mousewheel',
this.mousewheelListener,
this.options ? this.options : this.props.useCapture,
);
}
detachScrollListener() {
let scrollEl = window;
if (this.props.useWindow === false) {
scrollEl = this.getParentElement(this.scrollComponent);
}
scrollEl.removeEventListener(
'scroll',
this.scrollListener,
this.options ? this.options : this.props.useCapture,
);
scrollEl.removeEventListener(
'resize',
this.scrollListener,
this.options ? this.options : this.props.useCapture,
);
}
getParentElement(el) {
if (this.props.parent instanceof HTMLElement) {
return this.props.parent;
}
if (typeof this.props.parent === 'string') {
const elm = document.getElementById(this.props.parent);
if (elm !== null) return elm;
}
const scrollParent =
this.props.getScrollParent && this.props.getScrollParent();
if (scrollParent != null) {
return scrollParent;
}
return el && el.parentNode;
}
attachScrollListener() {
const parentElement = this.getParentElement(this.scrollComponent);
if (!this.props.hasMore || !parentElement) return;
let scrollEl = window;
if (this.props.useWindow === false) {
scrollEl = parentElement;
}
scrollEl.addEventListener(
'mousewheel',
this.mousewheelListener,
this.options ? this.options : this.props.useCapture,
);
scrollEl.addEventListener(
'scroll',
this.scrollListener,
this.options ? this.options : this.props.useCapture,
);
scrollEl.addEventListener(
'resize',
this.scrollListener,
this.options ? this.options : this.props.useCapture,
);
if (this.props.initialLoad) {
this.scrollListener();
}
}
/*
Prevents Chrome hangups
https://github.com/TryGhost/Ghost/issues/7934
*/
mousewheelListener(e) {
if (e.deltaY === 1 && !this.isPassiveSupported()) {
e.preventDefault();
}
}
onScrollListener() {
const el = this.scrollComponent;
const scrollEl = window;
const parentNode = this.getParentElement(el);
// terminate if not data or under process
if (
(typeof this.props.dataLength === 'number' && this.actionTriggered) ||
!this.props.hasMore
) {
return;
}
let offset;
if (this.props.useWindow) {
const doc =
document.documentElement || document.body.parentNode || document.body;
const scrollTop =
scrollEl.pageYOffset !== undefined
? scrollEl.pageYOffset
: doc.scrollTop;
offset = this.calculateOffset(el, scrollTop);
} else {
offset = el.scrollHeight - parentNode.scrollTop - parentNode.clientHeight;
}
if (
offset < Number(this.props.threshold) &&
el &&
el.offsetParent !== null
) {
this.detachScrollListener();
this.beforeScrollHeight = parentNode.scrollHeight;
this.beforeScrollTop = parentNode.scrollTop;
if (typeof this.props.loadMore === 'function') {
this.props.loadMore();
if (typeof this.props.dataLength === 'number') {
this.actionTriggered = true;
}
}
}
}
calculateOffset(el, scrollTop) {
if (!el) return 0;
return (
this.calculateTopPosition(el) +
(el.offsetHeight - scrollTop - window.innerHeight)
);
}
calculateTopPosition(el) {
if (!el) return 0;
return el.offsetTop + this.calculateTopPosition(el.offsetParent);
}
render() {
const renderProps = this.props;
const {
children,
element,
hasMore,
initialLoad,
loader,
loadMore,
ref,
threshold,
useCapture,
useWindow,
getScrollParent,
parent,
dataLength,
...props
} = renderProps;
props.ref = node => {
this.scrollComponent = node;
if (ref) {
ref(node);
}
};
// return children if no any element defined
if (!element) {
return <>{children}</>;
}
return React.createElement(element, props, [children]);
}
}
const InfiniteScroll = props => {
const { loader, ...rest } = props;
return (
<>
<InfiniteScrollComponent {...rest} />
{props.hasMore && loader ? loader : null}
</>
);
};
InfiniteScroll.defaultProps = {
element: 'div',
hasMore: false,
initialLoad: true,
loader: null,
ref: null,
threshold: 250,
useWindow: true,
useCapture: false,
getScrollParent: null,
parent: null,
dataLength: null,
};
InfiniteScroll.propTypes = {
children: PropTypes.node.isRequired,
element: PropTypes.node,
hasMore: PropTypes.bool,
initialLoad: PropTypes.bool,
loader: PropTypes.node,
loadMore: PropTypes.func.isRequired,
ref: PropTypes.func,
getScrollParent: PropTypes.func,
threshold: PropTypes.number,
useCapture: PropTypes.bool,
useWindow: PropTypes.bool,
parent: PropTypes.any,
dataLength: PropTypes.number,
};
export default InfiniteScroll;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment