diff --git a/lib/components/split-pane.tsx b/lib/components/split-pane.tsx index 28a74477..aea53f6f 100644 --- a/lib/components/split-pane.tsx +++ b/lib/components/split-pane.tsx @@ -1,224 +1,191 @@ -import React from 'react'; +import React, {useState, useEffect, useRef, forwardRef} from 'react'; import sum from 'lodash/sum'; import type {SplitPaneProps} from '../../typings/hyper'; -export default class SplitPane extends React.PureComponent< - React.PropsWithChildren, - {dragging: boolean} -> { - dragPanePosition!: number; - dragTarget!: Element; - panes!: Element[]; - paneIndex!: number; - d1!: 'height' | 'width'; - d2!: 'top' | 'left'; - d3!: 'clientX' | 'clientY'; - panesSize!: number; - dragging!: boolean; - constructor(props: SplitPaneProps) { - super(props); - this.state = {dragging: false}; - } +const SplitPane = forwardRef((props, ref) => { + const dragPanePosition = useRef(0); + const dragTarget = useRef(null); + const paneIndex = useRef(0); + const d1 = props.direction === 'horizontal' ? 'height' : 'width'; + const d2 = props.direction === 'horizontal' ? 'top' : 'left'; + const d3 = props.direction === 'horizontal' ? 'clientY' : 'clientX'; + const panesSize = useRef(null); + const [dragging, setDragging] = useState(false); - componentDidUpdate(prevProps: SplitPaneProps) { - if (this.state.dragging && prevProps.sizes !== this.props.sizes) { - // recompute positions for ongoing dragging - this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2]; - } - } - - setupPanes(ev: React.MouseEvent) { - const target = ev.target as HTMLDivElement; - this.panes = Array.from(target.parentElement?.children || []); - this.paneIndex = this.panes.indexOf(target); - this.paneIndex -= Math.ceil(this.paneIndex / 2); - } - - handleAutoResize = (ev: React.MouseEvent) => { + const handleAutoResize = (ev: React.MouseEvent, index: number) => { ev.preventDefault(); - this.setupPanes(ev); + paneIndex.current = index; - const sizes_ = this.getSizes(); - sizes_[this.paneIndex] = 0; - sizes_[this.paneIndex + 1] = 0; + const sizes_ = getSizes(); + sizes_[paneIndex.current] = 0; + sizes_[paneIndex.current + 1] = 0; const availableWidth = 1 - sum(sizes_); - sizes_[this.paneIndex] = availableWidth / 2; - sizes_[this.paneIndex + 1] = availableWidth / 2; + sizes_[paneIndex.current] = availableWidth / 2; + sizes_[paneIndex.current + 1] = availableWidth / 2; - this.props.onResize(sizes_); + props.onResize(sizes_); }; - handleDragStart = (ev: React.MouseEvent) => { + const handleDragStart = (ev: React.MouseEvent, index: number) => { ev.preventDefault(); - this.setState({dragging: true}); - window.addEventListener('mousemove', this.onDrag); - window.addEventListener('mouseup', this.onDragEnd); - - // dimensions to consider - if (this.props.direction === 'horizontal') { - this.d1 = 'height'; - this.d2 = 'top'; - this.d3 = 'clientY'; - } else { - this.d1 = 'width'; - this.d2 = 'left'; - this.d3 = 'clientX'; - } + setDragging(true); + window.addEventListener('mousemove', onDrag); + window.addEventListener('mouseup', onDragEnd); const target = ev.target as HTMLDivElement; - this.dragTarget = target; - this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2]; - this.panesSize = target.parentElement!.getBoundingClientRect()[this.d1]; - this.setupPanes(ev); + dragTarget.current = target; + dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2]; + panesSize.current = target.parentElement!.getBoundingClientRect()[d1]; + paneIndex.current = index; }; - getSizes() { - const {sizes} = this.props; + const getSizes = () => { + const {sizes} = props; let sizes_: number[]; if (sizes) { sizes_ = [...sizes.asMutable()]; } else { - const total = (this.props.children as React.ReactNodeArray).length; + const total = props.children.length; const count = new Array(total).fill(1 / total); sizes_ = count; } return sizes_; - } + }; - onDrag = (ev: MouseEvent) => { - const sizes_ = this.getSizes(); + const onDrag = (ev: MouseEvent) => { + const sizes_ = getSizes(); - const i = this.paneIndex; - const pos = ev[this.d3]; - const d = Math.abs(this.dragPanePosition - pos) / this.panesSize; - if (pos > this.dragPanePosition) { + const i = paneIndex.current; + const pos = ev[d3]; + const d = Math.abs(dragPanePosition.current - pos) / panesSize.current!; + if (pos > dragPanePosition.current) { sizes_[i] += d; sizes_[i + 1] -= d; } else { sizes_[i] -= d; sizes_[i + 1] += d; } - this.props.onResize(sizes_); + props.onResize(sizes_); }; - onDragEnd = () => { - if (this.state.dragging) { - window.removeEventListener('mousemove', this.onDrag); - window.removeEventListener('mouseup', this.onDragEnd); - this.setState({dragging: false}); - } + const onDragEnd = () => { + window.removeEventListener('mousemove', onDrag); + window.removeEventListener('mouseup', onDragEnd); + setDragging(false); }; - render() { - const children = this.props.children as React.ReactNodeArray; - const {direction, borderColor} = this.props; - const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; - // workaround for the fact that if we don't specify - // sizes, sometimes flex fails to calculate the - // right height for the horizontal panes - const sizes = this.props.sizes || new Array(children.length).fill(1 / children.length); - return ( -
- {React.Children.map(children, (child, i) => { - const style = { - // flexBasis doesn't work for the first horizontal pane, height need to be specified - [sizeProperty]: `${sizes[i] * 100}%`, - flexBasis: `${sizes[i] * 100}%`, - flexGrow: 0 - }; - return [ -
+ useEffect(() => { + return () => { + onDragEnd(); + }; + }, []); + + const {children, direction, borderColor} = props; + const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; + // workaround for the fact that if we don't specify + // sizes, sometimes flex fails to calculate the + // right height for the horizontal panes + const sizes = props.sizes || new Array(children.length).fill(1 / children.length); + return ( +
+ {children.map((child, i) => { + const style = { + // flexBasis doesn't work for the first horizontal pane, height need to be specified + [sizeProperty]: `${sizes[i] * 100}%`, + flexBasis: `${sizes[i] * 100}%`, + flexGrow: 0 + }; + + return ( + +
{child} -
, - i < children.length - 1 ? ( +
+ {i < children.length - 1 ? (
handleDragStart(e, i)} + onDoubleClick={(e) => handleAutoResize(e, i)} style={{backgroundColor: borderColor}} className={`splitpane_divider splitpane_divider_${direction}`} /> - ) : null - ]; - })} -
+ ) : null} + + ); + })} +
- -
- ); - } + /* + this shim is used to make sure mousemove events + trigger in all the draggable area of the screen + this is not the case due to hterm's