-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e75871a
commit 1c92452
Showing
2 changed files
with
149 additions
and
181 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters