hyper/lib/components/split-pane.js

190 lines
4.8 KiB
JavaScript

import React from 'react';
import Component from '../component';
export default class SplitPane extends Component {
constructor(props) {
super(props);
this.handleDragStart = this.handleDragStart.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.state = {dragging: false};
}
componentDidUpdate(prevProps) {
if (this.state.dragging && prevProps.sizes !== this.props.sizes) {
// recompute positions for ongoing dragging
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
}
}
handleDragStart(ev) {
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';
}
this.dragTarget = ev.target;
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
this.panes = Array.from(ev.target.parentNode.childNodes);
this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1];
this.paneIndex = this.panes.indexOf(ev.target);
this.paneIndex -= Math.ceil(this.paneIndex / 2);
}
onDrag(ev) {
let {sizes} = this.props;
let sizes_;
if (sizes) {
sizes_ = [].concat(sizes);
} else {
const total = this.props.children.length;
sizes = sizes_ = new Array(total).fill(1 / total);
}
const i = this.paneIndex;
const pos = ev[this.d3];
const d = Math.abs(this.dragPanePosition - pos) / this.panesSize;
if (pos > this.dragPanePosition) {
sizes_[i] += d;
sizes_[i + 1] -= d;
} else {
sizes_[i] -= d;
sizes_[i + 1] += d;
}
this.props.onResize(sizes_);
}
onDragEnd() {
if (this.state.dragging) {
window.removeEventListener('mousemove', this.onDrag);
window.removeEventListener('mouseup', this.onDragEnd);
this.setState({dragging: false});
}
}
template(css) {
const children = this.props.children;
const {direction, borderColor} = this.props;
let {sizes} = this.props;
if (!sizes) {
// workaround for the fact that if we don't specify
// sizes, sometimes flex fails to calculate the
// right height for the horizontal panes
sizes = new Array(children.length).fill(1 / children.length);
}
return (<div className={css('panes', `panes_${direction}`)}>
{
React.Children.map(children, (child, i) => {
const style = {
flexBasis: (sizes[i] * 100) + '%',
flexGrow: 0
};
return [
<div
key="pane"
className={css('pane')}
style={style}
>
{ child }
</div>,
i < children.length - 1 ?
<div
key="divider"
onMouseDown={this.handleDragStart}
style={{backgroundColor: borderColor}}
className={css('divider', `divider_${direction}`)}
/> :
null
];
})
}
<div
style={{display: this.state.dragging ? 'block' : 'none'}}
className={css('shim')}
/>
</div>);
}
styles() {
return {
panes: {
display: 'flex',
flex: 1,
outline: 'none',
position: 'relative',
width: '100%',
height: '100%'
},
'panes_vertical': {
flexDirection: 'row'
},
'panes_horizontal': {
flexDirection: 'column'
},
pane: {
flex: 1,
outline: 'none',
position: 'relative'
},
divider: {
boxSizing: 'border-box',
zIndex: '1',
backgroundClip: 'padding-box',
flexShrink: 0
},
'divider_vertical': {
borderLeft: '5px solid rgba(255, 255, 255, 0)',
borderRight: '5px solid rgba(255, 255, 255, 0)',
width: '11px',
margin: '0 -5px',
cursor: 'col-resize'
},
'divider_horizontal': {
height: '11px',
margin: '-5px 0',
borderTop: '5px solid rgba(255, 255, 255, 0)',
borderBottom: '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>
shim: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'transparent'
}
};
}
componentWillUnmount() {
// ensure drag end
if (this.dragging) {
this.onDragEnd();
}
}
}