mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
split-pane > function component
This commit is contained in:
parent
e75871aa07
commit
1c92452e61
2 changed files with 139 additions and 171 deletions
|
|
@ -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};
|
||||
}
|
||||
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);
|
||||
|
||||
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 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" />
|
||||
) : 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%;
|
||||
}
|
||||
<style jsx>{`
|
||||
.splitpane_panes {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splitpane_panes_vertical {
|
||||
flex-direction: row;
|
||||
}
|
||||
.splitpane_panes_vertical {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.splitpane_panes_horizontal {
|
||||
flex-direction: column;
|
||||
}
|
||||
.splitpane_panes_horizontal {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitpane_pane {
|
||||
flex: 1;
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
.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 {
|
||||
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_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%;
|
||||
}
|
||||
.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>
|
||||
);
|
||||
}
|
||||
/*
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
SplitPane.displayName = 'SplitPane';
|
||||
|
||||
export default SplitPane;
|
||||
|
|
|
|||
5
typings/hyper.d.ts
vendored
5
typings/hyper.d.ts
vendored
|
|
@ -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[];
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue