{"version":3,"file":"index.module.mjs","sources":["../src/index.tsx"],"sourcesContent":["import * as React from 'react';\nimport { CSSProperties } from 'react';\n\nexport type ScrollableFeedProps = {\n    forceScroll?: boolean;\n    animateScroll?: (element: HTMLElement, offset: number) => void;\n    onScrollComplete?: () => void;\n    changeDetectionFilter?: (previousProps: ScrollableFeedComponentProps, newProps: ScrollableFeedComponentProps) => boolean;\n    viewableDetectionEpsilon?: number;\n    className?: string;\n    onScroll?: (isAtBottom: boolean) => void;\n    debug?: boolean\n}\n\ntype ScrollableFeedComponentProps = React.PropsWithChildren<ScrollableFeedProps>;\n\nclass ScrollableFeed extends React.Component<React.PropsWithChildren<ScrollableFeedProps>> {\n    private readonly wrapperRef: React.RefObject<HTMLDivElement | null>;\n    private readonly bottomRef: React.RefObject<HTMLDivElement | null>;\n\n    constructor(props: ScrollableFeedProps) {\n        super(props);\n        this.bottomRef = React.createRef();\n        this.wrapperRef = React.createRef();\n        this.handleScroll = this.handleScroll.bind(this);\n\n        if (this.props.debug) console.log(\"Component cstr\");\n    }\n\n    static defaultProps: ScrollableFeedProps = {\n        forceScroll: false,\n        animateScroll: (element: HTMLElement, offset: number): void => {\n            if (element.scrollBy) {\n                element.scrollBy({ top: offset });\n            }\n            else {\n                element.scrollTop = offset;\n            }\n        },\n        onScrollComplete: () => {},\n        changeDetectionFilter: () => true,\n        viewableDetectionEpsilon: 2,\n        onScroll: () => {},\n    };\n\n    getSnapshotBeforeUpdate(): boolean {\n        if (this.props.debug) console.log(\"Component \", this.getSnapshotBeforeUpdate.name);\n        if (this.wrapperRef.current && this.bottomRef.current) {\n            const { viewableDetectionEpsilon } = this.props;\n            return ScrollableFeed.isViewable(this.wrapperRef.current, this.bottomRef.current, viewableDetectionEpsilon!); //This argument is passed down to componentDidUpdate as 3rd parameter\n        }\n        return false;\n    }\n\n    componentDidUpdate(previousProps: ScrollableFeedComponentProps, _previousState: any, snapshot: boolean): void {\n        if (this.props.debug) console.log(\"Component \", this.componentDidUpdate.name);\n        const { forceScroll, changeDetectionFilter } = this.props;\n        const isValidChange = changeDetectionFilter!(previousProps, this.props);\n        if (isValidChange && (forceScroll || snapshot) && this.bottomRef.current && this.wrapperRef.current) {\n            this.scrollParentToChild(this.wrapperRef.current, this.bottomRef.current);\n        }\n    }\n\n    componentDidMount(): void {\n        if (this.props.debug) console.log(\"Component \", this.componentDidMount.name);\n        //Scroll to bottom from the start\n        if (this.bottomRef.current && this.wrapperRef.current) {\n            this.scrollParentToChild(this.wrapperRef.current, this.bottomRef.current);\n        }\n    }\n\n    /**\n     * Scrolls a parent element such that the child element will be in view\n     * @param parent\n     * @param child\n     */\n    protected scrollParentToChild(parent: HTMLElement, child: HTMLElement): void {\n        const { viewableDetectionEpsilon } = this.props;\n        if (!ScrollableFeed.isViewable(parent, child, viewableDetectionEpsilon!)) {\n            //Source: https://stackoverflow.com/a/45411081/6316091\n            const parentRect = parent.getBoundingClientRect();\n            const childRect = child.getBoundingClientRect();\n\n            //Scroll by offset relative to parent\n            const scrollOffset = (childRect.top + parent.scrollTop) - parentRect.top;\n            const { animateScroll, onScrollComplete } = this.props;\n            if (animateScroll) {\n                animateScroll(parent, scrollOffset);\n                onScrollComplete!();\n            }\n        }\n    }\n\n    /**\n     * Returns whether a child element is visible within a parent element\n     *\n     * @param parent\n     * @param child\n     * @param epsilon\n     */\n    private static isViewable(parent: HTMLElement, child: HTMLElement, epsilon: number): boolean {\n        epsilon = epsilon || 0;\n\n        //Source: https://stackoverflow.com/a/45411081/6316091\n        const parentRect = parent.getBoundingClientRect();\n        const childRect = child.getBoundingClientRect();\n\n        const childTopIsViewable = (childRect.top >= parentRect.top);\n\n        const childOffsetToParentBottom = parentRect.top + parent.clientHeight - childRect.top;\n        const childBottomIsViewable = childOffsetToParentBottom + epsilon >= 0;\n\n        return childTopIsViewable && childBottomIsViewable;\n    }\n\n    /**\n     * Fires the onScroll event, sending isAtBottom boolean as its first parameter\n     */\n    protected handleScroll(): void {\n        const { viewableDetectionEpsilon, onScroll } = this.props;\n        if (onScroll && this.bottomRef.current && this.wrapperRef.current) {\n            const isAtBottom = ScrollableFeed.isViewable(this.wrapperRef.current, this.bottomRef.current, viewableDetectionEpsilon!);\n            onScroll(isAtBottom);\n        }\n    }\n\n    /**\n     * Scroll to the bottom\n     */\n    public scrollToBottom(): void {\n        if (this.bottomRef.current && this.wrapperRef.current) {\n            this.scrollParentToChild(this.wrapperRef.current, this.bottomRef.current);\n        }\n    }\n\n    render(): React.ReactNode {\n        if (this.props.debug) console.log(\"Component \", this.render.name);\n\n        const style: CSSProperties = {\n            maxHeight: \"inherit\",\n            height: \"inherit\",\n            overflowY: \"auto\",\n        };\n        const { children, className } = this.props;\n        return (\n            <div className={className} style={style} ref={this.wrapperRef} onScroll={this.handleScroll}>\n                {children}\n                <div ref={this.bottomRef}></div>\n            </div>\n        );\n    }\n}\n\nexport default ScrollableFeed;\n"],"names":["ScrollableFeed","_React$Component","props","_this","call","wrapperRef","bottomRef","React","createRef","handleScroll","bind","debug","console","log","_proto","prototype","getSnapshotBeforeUpdate","this","name","current","isViewable","viewableDetectionEpsilon","componentDidUpdate","previousProps","_previousState","snapshot","_this$props","forceScroll","changeDetectionFilter","scrollParentToChild","componentDidMount","parent","child","parentRect","getBoundingClientRect","childRect","_this$props2","animateScroll","onScrollComplete","top","scrollTop","epsilon","clientHeight","_this$props3","onScroll","scrollToBottom","render","_this$props4","className","style","maxHeight","height","overflowY","ref","children","createElement","Component","defaultProps","element","offset","scrollBy"],"mappings":"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"}