import * as React from 'react' import * as cx from 'classnames' import { connect } from 'react-redux' import { throttle } from 'lodash' import { StylableComponent } from '../theme' import { ScrollListScrollState } from './reducers' import { ScrollListProps, ScrollListComponentProps } from './interfaces' import { getScrollListById } from './selectors' import { scrollListScroll, scrollListUpdate } from './actions' import { childrenChanged, forceArray } from '../../utils' const styles = require('../../../src/components/scroll-list/styles.scss') class Component extends StylableComponent { private list: HTMLDivElement private getHeight = throttle(() => this.list.getBoundingClientRect().height, 200) private jumping = false public componentDidMount() { this.update() } public componentDidUpdate(prevProps: ScrollListComponentProps) { const { scrollList } = this.props if (childrenChanged(prevProps.children, this.props.children)) { this.update() } if (scrollList.jumped !== false) { this.jumping = true this.list.scrollTop = scrollList.scroll.scrollTop } } public render() { const { height, style } = this.props const skip = this.getSkip() const limit = this.getLimit(skip) return (
{forceArray(this.props.children).slice(skip, limit)}
) } private getSkip() { const { scrollList } = this.props return scrollList ? scrollList.scroll.skip : 0 } private getLimit(skip: number) { const { maxItemsToRender } = this.props return skip + maxItemsToRender } private setListRef = (e: any) => this.list = e private maxScrollTop(): number { const { scrollList, children } = this.props if (!scrollList) { return 0 } const { childHeight, childrenVisible } = scrollList return (childHeight * forceArray(children).length) - (childrenVisible * childHeight) } private getScrollState(): ScrollListScrollState { const { scrollList, maxItemsToRender, children } = this.props if (!scrollList) { return { scrollTop: 0, idx: 0, skip: 0 } } const { childHeight, childrenVisible, scroll } = scrollList const scrollTop = Math.min(this.list.scrollTop, this.maxScrollTop()) const idx = Math.max( Math.min( scroll.skip + Math.floor(scrollTop / childHeight), forceArray(children).length - childrenVisible, ), 0, ) let skip = scroll.skip if (scroll.idx + (childrenVisible * 2) > this.getLimit(this.getSkip())) { skip = Math.min( scroll.skip + (maxItemsToRender - childrenVisible), forceArray(this.props.children).length - childrenVisible, ) } else if (idx - childrenVisible < scroll.skip) { skip = Math.max(scroll.skip - childrenVisible, 0) } return { scrollTop, skip: Math.max(0, Math.min(skip, idx - childrenVisible)), idx, } } private handleScroll = (e: React.SyntheticEvent): void => { if (this.jumping) { this.jumping = false this.update() } else { this.props.scrollListScroll(this.props.id, this.getScrollState()) } } private update() { const height = this.getHeight() const childHeight = this.props.childHeight const childrenVisible = Math.ceil(height / childHeight) if (this.props.maxItemsToRender < childrenVisible * 4) { console.error('maxItemsToRender should be no less than the amount of items visible * 4') } const scroll = this.getScrollState() this.props.scrollListUpdate(this.props.id, { height, childHeight, scroll, childrenVisible: Math.ceil(height / childHeight), jumped: this.maxScrollTop() < scroll.scrollTop ? scroll.idx : false, }) } } const mapStateToProps = (state: any, ownProps: ScrollListProps) => ({ ...ownProps, scrollList: getScrollListById(state, ownProps.id), }) const mapDispatchToProps = { scrollListScroll, scrollListUpdate, } export const ScrollList = connect(mapStateToProps, mapDispatchToProps)(Component)