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,224 +1,191 @@
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 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 {children, direction, borderColor} = props;
const sizes = this.props.sizes || new Array<number>(children.length).fill(1 / children.length); const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
return ( // workaround for the fact that if we don't specify
<div className={`splitpane_panes splitpane_panes_${direction}`}> // sizes, sometimes flex fails to calculate the
{React.Children.map(children, (child, i) => { // right height for the horizontal panes
const style = { const sizes = props.sizes || new Array<number>(children.length).fill(1 / children.length);
// flexBasis doesn't work for the first horizontal pane, height need to be specified return (
[sizeProperty]: `${sizes[i] * 100}%`, <div className={`splitpane_panes splitpane_panes_${direction}`} ref={ref}>
flexBasis: `${sizes[i] * 100}%`, {children.map((child, i) => {
flexGrow: 0 const style = {
}; // flexBasis doesn't work for the first horizontal pane, height need to be specified
return [ [sizeProperty]: `${sizes[i] * 100}%`,
<div key="pane" className="splitpane_pane" style={style}> flexBasis: `${sizes[i] * 100}%`,
flexGrow: 0
};
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 {
display: flex; display: flex;
flex: 1; flex: 1;
outline: none; outline: none;
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.splitpane_panes_vertical { .splitpane_panes_vertical {
flex-direction: row; flex-direction: row;
} }
.splitpane_panes_horizontal { .splitpane_panes_horizontal {
flex-direction: column; flex-direction: column;
} }
.splitpane_pane { .splitpane_pane {
flex: 1; flex: 1;
outline: none; outline: none;
position: relative; position: relative;
} }
.splitpane_divider { .splitpane_divider {
box-sizing: border-box; box-sizing: border-box;
z-index: 1; z-index: 1;
background-clip: padding-box; background-clip: padding-box;
flex-shrink: 0; flex-shrink: 0;
} }
.splitpane_divider_vertical { .splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0); border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0); border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px; width: 11px;
margin: 0 -5px; margin: 0 -5px;
cursor: col-resize; cursor: col-resize;
} }
.splitpane_divider_horizontal { .splitpane_divider_horizontal {
height: 11px; height: 11px;
margin: -5px 0; margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0); border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0); border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize; cursor: row-resize;
width: 100%; width: 100%;
} }
/* /*
this shim is used to make sure mousemove events this shim is used to make sure mousemove events
trigger in all the draggable area of the screen trigger in all the draggable area of the screen
this is not the case due to hterm's <iframe> this is not the case due to hterm's <iframe>
*/ */
.splitpane_shim { .splitpane_shim {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: transparent; background: transparent;
} }
`}</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';