import * as React from "react";
import BaseScrollView, { ScrollEvent, ScrollViewDefaultProps } from "../../../core/scrollcomponent/BaseScrollView";
import debounce = require("lodash.debounce");
import { ScrollEventNormalizer } from "./ScrollEventNormalizer";
/***
* A scrollviewer that mimics react native scrollview. Additionally on web it can start listening to window scroll events optionally.
* Supports both window scroll and scrollable divs inside other divs.
*/
export default class ScrollViewer extends BaseScrollView {
public static defaultProps = {
canChangeSize: false,
horizontal: false,
style: null,
useWindowScroll: false,
};
private scrollEndEventSimulator = debounce((executable: () => void) => {
executable();
}, 1200);
private _mainDivRef: HTMLDivElement | null = null;
private _isScrolling: boolean = false;
private _scrollEventNormalizer: ScrollEventNormalizer | null = null;
public componentDidMount(): void {
if (this.props.onSizeChanged) {
if (this.props.useWindowScroll) {
this._startListeningToWindowEvents();
this.props.onSizeChanged({ height: window.innerHeight, width: window.innerWidth });
} else if (this._mainDivRef) {
this._startListeningToDivEvents();
this.props.onSizeChanged({ height: this._mainDivRef.clientHeight, width: this._mainDivRef.clientWidth });
}
}
}
public componentWillUnmount(): void {
window.removeEventListener("scroll", this._windowOnScroll);
if (this._mainDivRef) {
this._mainDivRef.removeEventListener("scroll", this._onScroll);
}
window.removeEventListener("resize", this._onWindowResize);
}
public scrollTo(scrollInput: { x: number, y: number, animated: boolean }): void {
if (scrollInput.animated) {
this._doAnimatedScroll(this.props.horizontal ? scrollInput.x : scrollInput.y);
} else {
this._setRelevantOffset(this.props.horizontal ? scrollInput.x : scrollInput.y);
}
}
public render(): JSX.Element {
return !this.props.useWindowScroll
?
:
{this.props.children}
;
}
private _setDivRef = (div: HTMLDivElement | null): void => {
this._mainDivRef = div;
if (div) {
this._scrollEventNormalizer = new ScrollEventNormalizer(div);
} else {
this._scrollEventNormalizer = null;
}
}
private _getRelevantOffset = (): number => {
if (!this.props.useWindowScroll) {
if (this._mainDivRef) {
if (this.props.horizontal) {
return this._mainDivRef.scrollLeft;
} else {
return this._mainDivRef.scrollTop;
}
}
return 0;
} else {
if (this.props.horizontal) {
return window.scrollX;
} else {
return window.scrollY;
}
}
}
private _setRelevantOffset = (offset: number): void => {
if (!this.props.useWindowScroll) {
if (this._mainDivRef) {
if (this.props.horizontal) {
this._mainDivRef.scrollLeft = offset;
} else {
this._mainDivRef.scrollTop = offset;
}
}
} else {
if (this.props.horizontal) {
window.scrollTo(offset, 0);
} else {
window.scrollTo(0, offset);
}
}
}
private _isScrollEnd = (): void => {
if (this._mainDivRef) {
this._mainDivRef.style.pointerEvents = "auto";
}
this._isScrolling = false;
}
private _trackScrollOccurence = (): void => {
if (!this._isScrolling) {
if (this._mainDivRef) {
this._mainDivRef.style.pointerEvents = "none";
}
this._isScrolling = true;
}
this.scrollEndEventSimulator(this._isScrollEnd);
}
private _doAnimatedScroll(offset: number): void {
let start = this._getRelevantOffset();
if (offset > start) {
start = Math.max(offset - 800, start);
} else {
start = Math.min(offset + 800, start);
}
const change = offset - start;
const increment = 20;
const duration = 200;
const animateScroll = (elapsedTime: number) => {
elapsedTime += increment;
const position = this._easeInOut(elapsedTime, start, change, duration);
this._setRelevantOffset(position);
if (elapsedTime < duration) {
window.setTimeout(() => animateScroll(elapsedTime), increment);
}
};
animateScroll(0);
}
private _startListeningToDivEvents(): void {
if (this._mainDivRef) {
this._mainDivRef.addEventListener("scroll", this._onScroll);
}
}
private _startListeningToWindowEvents(): void {
window.addEventListener("scroll", this._windowOnScroll);
if (this.props.canChangeSize) {
window.addEventListener("resize", this._onWindowResize);
}
}
private _onWindowResize = (): void => {
if (this.props.onSizeChanged && this.props.useWindowScroll) {
this.props.onSizeChanged({ height: window.innerHeight, width: window.innerWidth });
}
}
private _windowOnScroll = (): void => {
if (this.props.onScroll) {
if (this._scrollEventNormalizer) {
this.props.onScroll(this._scrollEventNormalizer.windowEvent);
}
}
}
private _onScroll = (): void => {
if (this.props.onScroll) {
if (this._scrollEventNormalizer) {
this.props.onScroll(this._scrollEventNormalizer.divEvent);
}
}
}
private _easeInOut(currentTime: number, start: number, change: number, duration: number): number {
currentTime /= duration / 2;
if (currentTime < 1) {
return change / 2 * currentTime * currentTime + start;
}
currentTime -= 1;
return (-change) / 2 * (currentTime * (currentTime - 2) - 1) + start;
}
}