split-pane > function component

This commit is contained in:
Labhansh Agrawal 2023-08-01 14:08:35 +05:30
parent e75871aa07
commit 1c92452e61
2 changed files with 139 additions and 171 deletions

View file

@ -1,153 +1,124 @@
import React from 'react'; import React, {useState, useEffect, useRef, forwardRef} from 'react';
import sum from 'lodash/sum'; import sum from 'lodash/sum';
import type {SplitPaneProps} from '../../typings/hyper'; import type {SplitPaneProps} from '../../typings/hyper';
export default class SplitPane extends React.PureComponent< const SplitPane = forwardRef<HTMLDivElement, SplitPaneProps>((props, ref) => {
React.PropsWithChildren<SplitPaneProps>, const dragPanePosition = useRef<number>(0);
{dragging: boolean} const dragTarget = useRef<HTMLDivElement | null>(null);
> { const paneIndex = useRef<number>(0);
dragPanePosition!: number; const d1 = props.direction === 'horizontal' ? 'height' : 'width';
dragTarget!: Element; const d2 = props.direction === 'horizontal' ? 'top' : 'left';
panes!: Element[]; const d3 = props.direction === 'horizontal' ? 'clientY' : 'clientX';
paneIndex!: number; const panesSize = useRef<number | null>(null);
d1!: 'height' | 'width'; const [dragging, setDragging] = useState(false);
d2!: 'top' | 'left';
d3!: 'clientX' | 'clientY';
panesSize!: number;
dragging!: boolean;
constructor(props: SplitPaneProps) {
super(props);
this.state = {dragging: false};
}
componentDidUpdate(prevProps: SplitPaneProps) { const handleAutoResize = (ev: React.MouseEvent<HTMLDivElement>, index: number) => {
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<HTMLDivElement>) {
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<HTMLDivElement>) => {
ev.preventDefault(); ev.preventDefault();
this.setupPanes(ev); paneIndex.current = index;
const sizes_ = this.getSizes(); const sizes_ = getSizes();
sizes_[this.paneIndex] = 0; sizes_[paneIndex.current] = 0;
sizes_[this.paneIndex + 1] = 0; sizes_[paneIndex.current + 1] = 0;
const availableWidth = 1 - sum(sizes_); const availableWidth = 1 - sum(sizes_);
sizes_[this.paneIndex] = availableWidth / 2; sizes_[paneIndex.current] = availableWidth / 2;
sizes_[this.paneIndex + 1] = availableWidth / 2; sizes_[paneIndex.current + 1] = availableWidth / 2;
this.props.onResize(sizes_); props.onResize(sizes_);
}; };
handleDragStart = (ev: React.MouseEvent<HTMLDivElement>) => { const handleDragStart = (ev: React.MouseEvent<HTMLDivElement>, index: number) => {
ev.preventDefault(); ev.preventDefault();
this.setState({dragging: true}); setDragging(true);
window.addEventListener('mousemove', this.onDrag); window.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', this.onDragEnd); window.addEventListener('mouseup', 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';
}
const target = ev.target as HTMLDivElement; const target = ev.target as HTMLDivElement;
this.dragTarget = target; dragTarget.current = target;
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2]; dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2];
this.panesSize = target.parentElement!.getBoundingClientRect()[this.d1]; panesSize.current = target.parentElement!.getBoundingClientRect()[d1];
this.setupPanes(ev); paneIndex.current = index;
}; };
getSizes() { const getSizes = () => {
const {sizes} = this.props; const {sizes} = props;
let sizes_: number[]; let sizes_: number[];
if (sizes) { if (sizes) {
sizes_ = [...sizes.asMutable()]; sizes_ = [...sizes.asMutable()];
} else { } else {
const total = (this.props.children as React.ReactNodeArray).length; const total = props.children.length;
const count = new Array<number>(total).fill(1 / total); const count = new Array<number>(total).fill(1 / total);
sizes_ = count; sizes_ = count;
} }
return sizes_; return sizes_;
} };
onDrag = (ev: MouseEvent) => { const onDrag = (ev: MouseEvent) => {
const sizes_ = this.getSizes(); const sizes_ = getSizes();
const i = this.paneIndex; const i = paneIndex.current;
const pos = ev[this.d3]; const pos = ev[d3];
const d = Math.abs(this.dragPanePosition - pos) / this.panesSize; const d = Math.abs(dragPanePosition.current - pos) / panesSize.current!;
if (pos > this.dragPanePosition) { if (pos > dragPanePosition.current) {
sizes_[i] += d; sizes_[i] += d;
sizes_[i + 1] -= d; sizes_[i + 1] -= d;
} else { } else {
sizes_[i] -= d; sizes_[i] -= d;
sizes_[i + 1] += d; sizes_[i + 1] += d;
} }
this.props.onResize(sizes_); props.onResize(sizes_);
}; };
onDragEnd = () => { const onDragEnd = () => {
if (this.state.dragging) { window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mousemove', this.onDrag); window.removeEventListener('mouseup', onDragEnd);
window.removeEventListener('mouseup', this.onDragEnd); setDragging(false);
this.setState({dragging: false});
}
}; };
render() { useEffect(() => {
const children = this.props.children as React.ReactNodeArray; return () => {
const {direction, borderColor} = this.props; onDragEnd();
};
}, []);
const {children, direction, borderColor} = props;
const sizeProperty = direction === 'horizontal' ? 'height' : 'width'; const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
// workaround for the fact that if we don't specify // workaround for the fact that if we don't specify
// sizes, sometimes flex fails to calculate the // sizes, sometimes flex fails to calculate the
// right height for the horizontal panes // right height for the horizontal panes
const sizes = this.props.sizes || new Array<number>(children.length).fill(1 / children.length); const sizes = props.sizes || new Array<number>(children.length).fill(1 / children.length);
return ( return (
<div className={`splitpane_panes splitpane_panes_${direction}`}> <div className={`splitpane_panes splitpane_panes_${direction}`} ref={ref}>
{React.Children.map(children, (child, i) => { {children.map((child, i) => {
const style = { const style = {
// flexBasis doesn't work for the first horizontal pane, height need to be specified // flexBasis doesn't work for the first horizontal pane, height need to be specified
[sizeProperty]: `${sizes[i] * 100}%`, [sizeProperty]: `${sizes[i] * 100}%`,
flexBasis: `${sizes[i] * 100}%`, flexBasis: `${sizes[i] * 100}%`,
flexGrow: 0 flexGrow: 0
}; };
return [
<div key="pane" className="splitpane_pane" style={style}> return (
<React.Fragment key={i}>
<div className="splitpane_pane" style={style}>
{child} {child}
</div>, </div>
i < children.length - 1 ? ( {i < children.length - 1 ? (
<div <div
key="divider" onMouseDown={(e) => handleDragStart(e, i)}
onMouseDown={this.handleDragStart} onDoubleClick={(e) => handleAutoResize(e, i)}
onDoubleClick={this.handleAutoResize}
style={{backgroundColor: borderColor}} style={{backgroundColor: borderColor}}
className={`splitpane_divider splitpane_divider_${direction}`} className={`splitpane_divider splitpane_divider_${direction}`}
/> />
) : null ) : null}
]; </React.Fragment>
);
})} })}
<div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" /> <div style={{display: dragging ? 'block' : 'none'}} className="splitpane_shim" />
<style jsx>{` <style jsx>{`
.splitpane_panes { .splitpane_panes {
@ -213,12 +184,8 @@ export default class SplitPane extends React.PureComponent<
`}</style> `}</style>
</div> </div>
); );
} });
componentWillUnmount() { SplitPane.displayName = 'SplitPane';
// ensure drag end
if (this.dragging) { export default SplitPane;
this.onDragEnd();
}
}
}

5
typings/hyper.d.ts vendored
View file

@ -192,7 +192,7 @@ export type HyperActions = (
import type configureStore from '../lib/store/configure-store'; import type configureStore from '../lib/store/configure-store';
export type HyperDispatch = ReturnType<typeof configureStore>['dispatch']; export type HyperDispatch = ReturnType<typeof configureStore>['dispatch'];
import type {ReactChild} from 'react'; import type {ReactChild, ReactNode} from 'react';
type extensionProps = Partial<{ type extensionProps = Partial<{
customChildren: ReactChild | ReactChild[]; customChildren: ReactChild | ReactChild[];
customChildrenBefore: ReactChild | ReactChild[]; customChildrenBefore: ReactChild | ReactChild[];
@ -264,8 +264,9 @@ export type NotificationProps = {
export type SplitPaneProps = { export type SplitPaneProps = {
borderColor: string; borderColor: string;
direction: 'horizontal' | 'vertical'; direction: 'horizontal' | 'vertical';
onResize: Function; onResize: (sizes: number[]) => void;
sizes?: Immutable<number[]> | null; sizes?: Immutable<number[]> | null;
children: ReactNode[];
}; };
import type Term from '../lib/components/term'; import type Term from '../lib/components/term';