diff --git a/src/components/PageExtractionModal/ListItem.tsx b/src/components/PageExtractionModal/ListItem.tsx index b5e0d9a..38919f7 100644 --- a/src/components/PageExtractionModal/ListItem.tsx +++ b/src/components/PageExtractionModal/ListItem.tsx @@ -1,20 +1,40 @@ import React, { createElement } from "react"; interface ListItemInputProps { - ref: any; item: any; render: any; - isVisible: boolean; + parentAddEventListener: any; + parentRemoveEventListener: any; } interface ListItemState { renderTarget: any; shouldRenderItem: boolean; + isVisible: boolean; } class ListItem extends React.Component { + private static MAX_SIZE = 0; + private _containerRef: React.RefObject; + private _measurementRef: React.RefObject; + private _resizeObserver: ResizeObserver; + private _height = 0; + private _scrollHandle: any; constructor(props: ListItemInputProps) { super(props); + this._containerRef = React.createRef(); + this._measurementRef = React.createRef(); + this._resizeObserver = new ResizeObserver(() => { + const rect = this._measurementRef.current?.getBoundingClientRect(); + if (!rect || rect.height === 0 || (this._height && rect.height < this._height)) { + return; + } + this._height = rect.height; + if (this._height > ListItem.MAX_SIZE) { + ListItem.MAX_SIZE = this._height; + } + }); + this.props.parentAddEventListener("scroll", this.onParentScroll); const renderTarget = this.props.render(this.props.item); const isPromise = renderTarget instanceof Promise; if (isPromise) { @@ -22,19 +42,69 @@ class ListItem extends React.Component { } this.state = { renderTarget: isPromise ? undefined : renderTarget, - shouldRenderItem: !isPromise + shouldRenderItem: !isPromise, + isVisible: true }; } + componentDidMount(): void { + // @ts-ignore + this._resizeObserver.observe(this._measurementRef.current); + } + componentWillUnmount(): void { + if (this._measurementRef.current) { + this._resizeObserver.unobserve(this._measurementRef.current); + } + this.props.parentRemoveEventListener("scroll", this.onParentScroll); + } + onParentScroll = (parentRect: any, _scrollTop: number, padding: number) => { + clearTimeout(this._scrollHandle); + this._scrollHandle = setTimeout(() => { + const rect = this._containerRef.current?.getBoundingClientRect(); + if (this.doRectanglesIntersect(parentRect, rect, padding)) { + this.setState({ isVisible: true }); + } else { + this.setState({ isVisible: false }); + } + }, 100); + }; + doRectanglesIntersect = (rect1: any, rect2: any, padding = 13): boolean => { + const itemPadding = ListItem.MAX_SIZE * padding; + const rect1Top = rect1.y - itemPadding; + const rect1Bottom = rect1.y + rect1.height + itemPadding; + const rect2Top = rect2.y; + const rect2Bottom = rect2.y + rect2.height; + + const verticalIntersection = rect1Top < rect2Bottom && rect1Bottom > rect2Top; + + return verticalIntersection; + }; render(): JSX.Element { - if (!this.props.isVisible || !this.state.shouldRenderItem) { + if (!this.state.shouldRenderItem) { return <>; } return (
- {this.state.renderTarget} +
+ {this.state.isVisible ? ( + this.state.renderTarget + ) : ( +
+ )} +
); } diff --git a/src/components/PageExtractionModal/VirtualList.tsx b/src/components/PageExtractionModal/VirtualList.tsx index 04cbad4..b0ba9d2 100644 --- a/src/components/PageExtractionModal/VirtualList.tsx +++ b/src/components/PageExtractionModal/VirtualList.tsx @@ -3,21 +3,59 @@ import ListItem from "./ListItem"; interface VirtualListInputProps { items: any[]; - numItems: number; + padding: number; render: any; height: string | number; } class VirtualList extends React.Component { - private scrollContainerRef: React.RefObject; + private _scrollContainerRef: React.RefObject; + private _eventListeners: Record; constructor(props: VirtualListInputProps) { super(props); - this.scrollContainerRef = React.createRef(); + this._scrollContainerRef = React.createRef(); + this._eventListeners = {}; } + componentDidMount(): void { + this.trigger("mount", this._scrollContainerRef.current?.getBoundingClientRect()); + } + addEventListener = (event: string, handler: any): void => { + if (!this._eventListeners[event]) { + this._eventListeners[event] = []; + } + if (handler) { + this._eventListeners[event].push(handler); + } + }; + removeEventListener = (event: string, handler: any): void => { + if (!handler || !this._eventListeners[event]) { + return; + } + this._eventListeners[event] = this._eventListeners[event].reduce((acc, existingHandler) => { + if (existingHandler !== handler) { + acc.push(existingHandler); + } + return acc; + }, []); + }; + trigger = (event: string, ...parameters: any[]): void => { + if (!this._eventListeners[event]) { + return; + } + this._eventListeners[event].forEach(handler => handler(...parameters)); + }; + onScroll = (): void => { + this.trigger( + "scroll", + this._scrollContainerRef.current?.getBoundingClientRect(), + this._scrollContainerRef.current?.scrollTop, + this.props.padding + ); + }; render(): JSX.Element { return (
{ overflow: "auto", height: this.props.height }} + onScroll={this.onScroll} > {this.props.items.map((item, i) => ( - + ))}
); diff --git a/src/components/PageExtractionModal/index.tsx b/src/components/PageExtractionModal/index.tsx index 9ec7edb..dea96fa 100644 --- a/src/components/PageExtractionModal/index.tsx +++ b/src/components/PageExtractionModal/index.tsx @@ -255,7 +255,7 @@ class PageExtractionModal extends React.Component