import * as React from 'react' ; import IReactComponentProps from '../../../common/structures/IReactComponentProps' ; import ResizeObserver , { DOMRect } from 'react-resize-observer' ; import { VirtualListHelper , IVirtualListHelperCalculations } from './helpers/VirtualListHelper' ; import classnames from 'classnames' ; import * as styles from './VirtualList.scss' ; import { ReactElement } from 'react' ; import { ScrollShadow } from '../../overlays/ScrollShadow/ScrollShadow' ; export interface IVirtualListProps extends IReactComponentProps { containerRenderer ? ( children , customRendererStyles ) : ReactElement < any > | null ; /** list data passed to the list item renderer */ data : any [ ] | undefined | null ; /** whether to disable the resize listener used to adjust calculated height (if true, it's highly recommended to set 'wrapperSizePx') */ disableResizeObserver ? : boolean ; /** whether to disable virtualized renderersFactory and draw all list items or not (if true) */ disableVirtualization ? : boolean ; /** the height of an individual list item (must be predictble, exact, and consistent) */ itemHeight : number ; /**
     * item render function
     * note: the element returned by this factory function *must* be able to have its 'style' attribute set
     */ itemRenderer ( listItemData , listItemIndex , customRendererStyles , extraData ) : React . ReactNode ; /** this is any extra data that needs to be passed to 'itemRenderer' */ itemRendererExtraData ? : any ; /** callback function for when list items are calculated but not necessarily rendered */ onCalculate ? ( calculations ) : void ; /** callback function for when list items are rendered */ onRender ? ( calculations ) : void ; /** the height (in pixels) for both before and after overscan areas */ overscan ? : number | 'auto' ; /** Whether to show alternating row stripes. */ striped ? : boolean ; /** manually set wrapper size in pixels (note: this will be overridden if disableResizeObserver is 'false', but should be used if 'disableResizeObserver' is set to 'true') */ wrapperSizePx ? : number ; /** wrapper render function used to generate the scrolling element */ wrapperRenderer ? ( children ) : ReactElement < any > ; } export interface IVirtualListState { containerSizePx : number ; itemsRenderedList : ReturnType < IVirtualListProps [ 'itemRenderer' ] > [ ] ; lastDataLen : number ; } export class VirtualList extends React . Component < IVirtualListProps , IVirtualListState > { static defaultProps : Partial < IVirtualListProps > = { disableResizeObserver : false , disableVirtualization : false , overscan : 'auto' , } ; protected _calcs ! : IVirtualListHelperCalculations ; protected _helper = new VirtualListHelper ( ) ; protected _scrollTop : number = 0 ; protected _wrapperRef : HTMLDivElement | null = null ; protected _wrapperSizePx : number ; constructor ( props ) { super ( props ) ; this . _wrapperSizePx = props . wrapperSizePx || 2000 ; this . state = { containerSizePx : 0 , itemsRenderedList : [ ] , lastDataLen : 0 , } ; this . setWrapperRef = this . setWrapperRef . bind ( this ) ; } setWrapperRef = ( element ) => { if ( this . _wrapperRef ) { this . _wrapperRef . removeEventListener ( 'scroll' , this . _onScroll as any ) ; } if ( element ) { this . _wrapperRef = element ; this . _wrapperRef . addEventListener ( 'scroll' , this . _onScroll as any , { passive : true , } ) ; } } ; componentDidMount ( ) { this . _onDidMountOrUpdate ( ) ; } componentWillUnmount ( ) { if ( this . _wrapperRef ) { this . _wrapperRef . removeEventListener ( 'scroll' , this . _onScroll as any ) ; } } componentDidUpdate ( prevProps , prevState ) { this . _onDidMountOrUpdate ( ) ; } UNSAFE_componentWillReceiveProps ( nextProps ) { if ( nextProps . data !== this . props . data || ( nextProps . data && nextProps . data . length !== this . state . lastDataLen ) || nextProps . itemHeight !== this . props . itemHeight || nextProps . itemRenderer !== this . props . itemRenderer || nextProps . overscan !== this . props . overscan || nextProps . itemRendererExtraData !== this . props . itemRendererExtraData ) { this . _calculateItemsAndRenderIfChanged ( nextProps , true ) ; } } protected _calculateItemsAndRenderIfChanged ( props , forceReRender ) { const prevCalcs = this . _calcs ; const calculations = this . _helper . calculateItems ( props . data ? props . data . length : 0 , props . itemHeight , this . _wrapperSizePx , this . _scrollTop , props . overscan , props . disableVirtualization ) ; if ( calculations . containerSizePx > 10000000 && this . _calcs && calculations . containerSizePx !== this . _calcs . containerSizePx ) { console . warn ( `The virtual height of ${ calculations . containerSizePx . toLocaleString ( ) }px is dangerously high and may result in scrolling and/or rendering issues.` ) ; } this . _calcs = calculations ; if ( ! props . data || ! props . data . length ) { this . setState ( ( ) => ( { containerSizePx : 0 , itemsRenderedList : [ ] , lastDataLen : 0 , } ) ) ; } else if ( forceReRender || ! prevCalcs || calculations . virtualStartIndexInt !== prevCalcs . virtualStartIndexInt || calculations . virtualEndIndexInt !== prevCalcs . virtualEndIndexInt || calculations . containerSizePx !== prevCalcs . containerSizePx || calculations . viewportItemsCount !== prevCalcs . viewportItemsCount ) { const itemsRenderedList = this . _helper . generateAndGetRenderedItems ( props . data , props . itemRenderer , calculations , props . itemRendererExtraData , ! ! props . striped ) ; this . setState ( ( ) => ( { containerSizePx : calculations . containerSizePx , itemsRenderedList : itemsRenderedList , lastDataLen : props . data ! . length , } ) ) ; } if ( this . props . onCalculate ) { this . props . onCalculate ( this . _calcs ) ; } } protected _onDidMountOrUpdate ( ) { if ( this . props . onRender ) { this . props . onRender ( this . _calcs ) ; } } protected _onScroll = ( event ) => { if ( this . _wrapperRef && event . target === this . _wrapperRef ) { this . _scrollTop = this . _wrapperRef . scrollTop ; this . _calculateItemsAndRenderIfChanged ( this . props , false ) ; } } ; protected _onResize = ( rect ) => { if ( rect . height !== this . _wrapperSizePx ) { this . _wrapperSizePx = this . _wrapperRef ? this . _wrapperRef . clientHeight : rect . height ; this . _calculateItemsAndRenderIfChanged ( this . props , false ) ; } } ; protected _renderContainer ( ) { const containerStyle = { minHeight : this . state . containerSizePx } ; const containerRenderer = this . props . containerRenderer && this . props . containerRenderer ( this . state . itemsRenderedList , containerStyle ) ; const containerClassName = classnames ( styles . VirtualList , { [ styles . VirtualList__Striped ] : this . props . striped , } ) ; return ( < > { ! this . props . disableResizeObserver && < ResizeObserver onResize = { this . _onResize } />} { containerRenderer ? React . cloneElement ( containerRenderer as React . ReactElement < any > , { ... containerRenderer . props , className : classnames ( containerRenderer . props . className || '' , containerClassName ) , style : { ... containerStyle , ... ( containerRenderer . props . style || { } ) } , } ) : ( < div className = { containerClassName } style = { containerStyle } > { this . state . itemsRenderedList } < /div>)} < />); } protected _renderWrapper ( ) { const wrapperChildren = this . _renderContainer ( ) ; const wrapperRenderer = this . props . wrapperRenderer && this . props . wrapperRenderer ( wrapperChildren ) ; const wrapperClassName = classnames ( styles . VirtualList_Wrapper , this . props . className ) ; return ( wrapperRenderer ? React . cloneElement ( wrapperRenderer as React . ReactElement < any > , { ... wrapperRenderer . props , className : classnames ( wrapperRenderer . props . className || '' , wrapperClassName ) , } ) : ( < div className = { wrapperClassName } > { wrapperChildren } < /div>)); } render ( ) { return ( < ScrollShadow refCallback = { this . setWrapperRef } > { this . _renderWrapper ( ) } < /ScrollShadow>); } }