Skip to content

Commit

Permalink
split-pane > function component
Browse files Browse the repository at this point in the history
  • Loading branch information
LabhanshAgrawal committed Aug 1, 2023
1 parent e75871a commit 1c92452
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 181 deletions.
325 changes: 146 additions & 179 deletions lib/components/split-pane.tsx
Original file line number Diff line number Diff line change
@@ -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<SplitPaneProps>,
{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};
}

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<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>) => {
const SplitPane = forwardRef<HTMLDivElement, SplitPaneProps>((props, ref) => {
const dragPanePosition = useRef<number>(0);
const dragTarget = useRef<HTMLDivElement | null>(null);
const paneIndex = useRef<number>(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<number | null>(null);
const [dragging, setDragging] = useState(false);

const handleAutoResize = (ev: React.MouseEvent<HTMLDivElement>, 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<HTMLDivElement>) => {
const handleDragStart = (ev: React.MouseEvent<HTMLDivElement>, 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<number>(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<number>(children.length).fill(1 / children.length);
return (
<div className={`splitpane_panes splitpane_panes_${direction}`}>
{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 [
<div key="pane" className="splitpane_pane" style={style}>
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<number>(children.length).fill(1 / children.length);
return (
<div className={`splitpane_panes splitpane_panes_${direction}`} ref={ref}>
{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 (
<React.Fragment key={i}>
<div className="splitpane_pane" style={style}>
{child}
</div>,
i < children.length - 1 ? (
</div>
{i < children.length - 1 ? (
<div
key="divider"
onMouseDown={this.handleDragStart}
onDoubleClick={this.handleAutoResize}
onMouseDown={(e) => handleDragStart(e, i)}
onDoubleClick={(e) => handleAutoResize(e, i)}
style={{backgroundColor: borderColor}}
className={`splitpane_divider splitpane_divider_${direction}`}
/>
) : null
];
})}
<div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" />

<style jsx>{`
.splitpane_panes {
display: flex;
flex: 1;
outline: none;
position: relative;
width: 100%;
height: 100%;
}
.splitpane_panes_vertical {
flex-direction: row;
}
.splitpane_panes_horizontal {
flex-direction: column;
}
.splitpane_pane {
flex: 1;
outline: none;
position: relative;
}
.splitpane_divider {
box-sizing: border-box;
z-index: 1;
background-clip: padding-box;
flex-shrink: 0;
}
.splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
.splitpane_divider_horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
/*
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 <iframe>
*/
.splitpane_shim {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
`}</style>
</div>
);
}

componentWillUnmount() {
// ensure drag end
if (this.dragging) {
this.onDragEnd();
}
}
}
) : null}
</React.Fragment>
);
})}
<div style={{display: dragging ? 'block' : 'none'}} className="splitpane_shim" />

<style jsx>{`
.splitpane_panes {
display: flex;
flex: 1;
outline: none;
position: relative;
width: 100%;
height: 100%;
}
.splitpane_panes_vertical {
flex-direction: row;
}
.splitpane_panes_horizontal {
flex-direction: column;
}
.splitpane_pane {
flex: 1;
outline: none;
position: relative;
}
.splitpane_divider {
box-sizing: border-box;
z-index: 1;
background-clip: padding-box;
flex-shrink: 0;
}
.splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
.splitpane_divider_horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
/*
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 <iframe>
*/
.splitpane_shim {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
`}</style>
</div>
);
});

SplitPane.displayName = 'SplitPane';

export default SplitPane;
5 changes: 3 additions & 2 deletions typings/hyper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export type HyperActions = (
import type configureStore from '../lib/store/configure-store';
export type HyperDispatch = ReturnType<typeof configureStore>['dispatch'];

import type {ReactChild} from 'react';
import type {ReactChild, ReactNode} from 'react';
type extensionProps = Partial<{
customChildren: ReactChild | ReactChild[];
customChildrenBefore: ReactChild | ReactChild[];
Expand Down Expand Up @@ -264,8 +264,9 @@ export type NotificationProps = {
export type SplitPaneProps = {
borderColor: string;
direction: 'horizontal' | 'vertical';
onResize: Function;
onResize: (sizes: number[]) => void;
sizes?: Immutable<number[]> | null;
children: ReactNode[];
};

import type Term from '../lib/components/term';
Expand Down

0 comments on commit 1c92452

Please sign in to comment.