Skip to content

Commit 1c92452

Browse files
split-pane > function component
1 parent e75871a commit 1c92452

File tree

2 files changed

+149
-181
lines changed

2 files changed

+149
-181
lines changed

lib/components/split-pane.tsx

Lines changed: 146 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,191 @@
1-
import React from 'react';
1+
import React, {useState, useEffect, useRef, forwardRef} from 'react';
22

33
import sum from 'lodash/sum';
44

55
import type {SplitPaneProps} from '../../typings/hyper';
66

7-
export default class SplitPane extends React.PureComponent<
8-
React.PropsWithChildren<SplitPaneProps>,
9-
{dragging: boolean}
10-
> {
11-
dragPanePosition!: number;
12-
dragTarget!: Element;
13-
panes!: Element[];
14-
paneIndex!: number;
15-
d1!: 'height' | 'width';
16-
d2!: 'top' | 'left';
17-
d3!: 'clientX' | 'clientY';
18-
panesSize!: number;
19-
dragging!: boolean;
20-
constructor(props: SplitPaneProps) {
21-
super(props);
22-
this.state = {dragging: false};
23-
}
24-
25-
componentDidUpdate(prevProps: SplitPaneProps) {
26-
if (this.state.dragging && prevProps.sizes !== this.props.sizes) {
27-
// recompute positions for ongoing dragging
28-
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
29-
}
30-
}
31-
32-
setupPanes(ev: React.MouseEvent<HTMLDivElement>) {
33-
const target = ev.target as HTMLDivElement;
34-
this.panes = Array.from(target.parentElement?.children || []);
35-
this.paneIndex = this.panes.indexOf(target);
36-
this.paneIndex -= Math.ceil(this.paneIndex / 2);
37-
}
38-
39-
handleAutoResize = (ev: React.MouseEvent<HTMLDivElement>) => {
7+
const SplitPane = forwardRef<HTMLDivElement, SplitPaneProps>((props, ref) => {
8+
const dragPanePosition = useRef<number>(0);
9+
const dragTarget = useRef<HTMLDivElement | null>(null);
10+
const paneIndex = useRef<number>(0);
11+
const d1 = props.direction === 'horizontal' ? 'height' : 'width';
12+
const d2 = props.direction === 'horizontal' ? 'top' : 'left';
13+
const d3 = props.direction === 'horizontal' ? 'clientY' : 'clientX';
14+
const panesSize = useRef<number | null>(null);
15+
const [dragging, setDragging] = useState(false);
16+
17+
const handleAutoResize = (ev: React.MouseEvent<HTMLDivElement>, index: number) => {
4018
ev.preventDefault();
4119

42-
this.setupPanes(ev);
20+
paneIndex.current = index;
4321

44-
const sizes_ = this.getSizes();
45-
sizes_[this.paneIndex] = 0;
46-
sizes_[this.paneIndex + 1] = 0;
22+
const sizes_ = getSizes();
23+
sizes_[paneIndex.current] = 0;
24+
sizes_[paneIndex.current + 1] = 0;
4725

4826
const availableWidth = 1 - sum(sizes_);
49-
sizes_[this.paneIndex] = availableWidth / 2;
50-
sizes_[this.paneIndex + 1] = availableWidth / 2;
27+
sizes_[paneIndex.current] = availableWidth / 2;
28+
sizes_[paneIndex.current + 1] = availableWidth / 2;
5129

52-
this.props.onResize(sizes_);
30+
props.onResize(sizes_);
5331
};
5432

55-
handleDragStart = (ev: React.MouseEvent<HTMLDivElement>) => {
33+
const handleDragStart = (ev: React.MouseEvent<HTMLDivElement>, index: number) => {
5634
ev.preventDefault();
57-
this.setState({dragging: true});
58-
window.addEventListener('mousemove', this.onDrag);
59-
window.addEventListener('mouseup', this.onDragEnd);
60-
61-
// dimensions to consider
62-
if (this.props.direction === 'horizontal') {
63-
this.d1 = 'height';
64-
this.d2 = 'top';
65-
this.d3 = 'clientY';
66-
} else {
67-
this.d1 = 'width';
68-
this.d2 = 'left';
69-
this.d3 = 'clientX';
70-
}
35+
setDragging(true);
36+
window.addEventListener('mousemove', onDrag);
37+
window.addEventListener('mouseup', onDragEnd);
7138

7239
const target = ev.target as HTMLDivElement;
73-
this.dragTarget = target;
74-
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
75-
this.panesSize = target.parentElement!.getBoundingClientRect()[this.d1];
76-
this.setupPanes(ev);
40+
dragTarget.current = target;
41+
dragPanePosition.current = dragTarget.current.getBoundingClientRect()[d2];
42+
panesSize.current = target.parentElement!.getBoundingClientRect()[d1];
43+
paneIndex.current = index;
7744
};
7845

79-
getSizes() {
80-
const {sizes} = this.props;
46+
const getSizes = () => {
47+
const {sizes} = props;
8148
let sizes_: number[];
8249

8350
if (sizes) {
8451
sizes_ = [...sizes.asMutable()];
8552
} else {
86-
const total = (this.props.children as React.ReactNodeArray).length;
53+
const total = props.children.length;
8754
const count = new Array<number>(total).fill(1 / total);
8855

8956
sizes_ = count;
9057
}
9158
return sizes_;
92-
}
59+
};
9360

94-
onDrag = (ev: MouseEvent) => {
95-
const sizes_ = this.getSizes();
61+
const onDrag = (ev: MouseEvent) => {
62+
const sizes_ = getSizes();
9663

97-
const i = this.paneIndex;
98-
const pos = ev[this.d3];
99-
const d = Math.abs(this.dragPanePosition - pos) / this.panesSize;
100-
if (pos > this.dragPanePosition) {
64+
const i = paneIndex.current;
65+
const pos = ev[d3];
66+
const d = Math.abs(dragPanePosition.current - pos) / panesSize.current!;
67+
if (pos > dragPanePosition.current) {
10168
sizes_[i] += d;
10269
sizes_[i + 1] -= d;
10370
} else {
10471
sizes_[i] -= d;
10572
sizes_[i + 1] += d;
10673
}
107-
this.props.onResize(sizes_);
74+
props.onResize(sizes_);
10875
};
10976

110-
onDragEnd = () => {
111-
if (this.state.dragging) {
112-
window.removeEventListener('mousemove', this.onDrag);
113-
window.removeEventListener('mouseup', this.onDragEnd);
114-
this.setState({dragging: false});
115-
}
77+
const onDragEnd = () => {
78+
window.removeEventListener('mousemove', onDrag);
79+
window.removeEventListener('mouseup', onDragEnd);
80+
setDragging(false);
11681
};
11782

118-
render() {
119-
const children = this.props.children as React.ReactNodeArray;
120-
const {direction, borderColor} = this.props;
121-
const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
122-
// workaround for the fact that if we don't specify
123-
// sizes, sometimes flex fails to calculate the
124-
// right height for the horizontal panes
125-
const sizes = this.props.sizes || new Array<number>(children.length).fill(1 / children.length);
126-
return (
127-
<div className={`splitpane_panes splitpane_panes_${direction}`}>
128-
{React.Children.map(children, (child, i) => {
129-
const style = {
130-
// flexBasis doesn't work for the first horizontal pane, height need to be specified
131-
[sizeProperty]: `${sizes[i] * 100}%`,
132-
flexBasis: `${sizes[i] * 100}%`,
133-
flexGrow: 0
134-
};
135-
return [
136-
<div key="pane" className="splitpane_pane" style={style}>
83+
useEffect(() => {
84+
return () => {
85+
onDragEnd();
86+
};
87+
}, []);
88+
89+
const {children, direction, borderColor} = props;
90+
const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
91+
// workaround for the fact that if we don't specify
92+
// sizes, sometimes flex fails to calculate the
93+
// right height for the horizontal panes
94+
const sizes = props.sizes || new Array<number>(children.length).fill(1 / children.length);
95+
return (
96+
<div className={`splitpane_panes splitpane_panes_${direction}`} ref={ref}>
97+
{children.map((child, i) => {
98+
const style = {
99+
// flexBasis doesn't work for the first horizontal pane, height need to be specified
100+
[sizeProperty]: `${sizes[i] * 100}%`,
101+
flexBasis: `${sizes[i] * 100}%`,
102+
flexGrow: 0
103+
};
104+
105+
return (
106+
<React.Fragment key={i}>
107+
<div className="splitpane_pane" style={style}>
137108
{child}
138-
</div>,
139-
i < children.length - 1 ? (
109+
</div>
110+
{i < children.length - 1 ? (
140111
<div
141-
key="divider"
142-
onMouseDown={this.handleDragStart}
143-
onDoubleClick={this.handleAutoResize}
112+
onMouseDown={(e) => handleDragStart(e, i)}
113+
onDoubleClick={(e) => handleAutoResize(e, i)}
144114
style={{backgroundColor: borderColor}}
145115
className={`splitpane_divider splitpane_divider_${direction}`}
146116
/>
147-
) : null
148-
];
149-
})}
150-
<div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" />
151-
152-
<style jsx>{`
153-
.splitpane_panes {
154-
display: flex;
155-
flex: 1;
156-
outline: none;
157-
position: relative;
158-
width: 100%;
159-
height: 100%;
160-
}
161-
162-
.splitpane_panes_vertical {
163-
flex-direction: row;
164-
}
165-
166-
.splitpane_panes_horizontal {
167-
flex-direction: column;
168-
}
169-
170-
.splitpane_pane {
171-
flex: 1;
172-
outline: none;
173-
position: relative;
174-
}
175-
176-
.splitpane_divider {
177-
box-sizing: border-box;
178-
z-index: 1;
179-
background-clip: padding-box;
180-
flex-shrink: 0;
181-
}
182-
183-
.splitpane_divider_vertical {
184-
border-left: 5px solid rgba(255, 255, 255, 0);
185-
border-right: 5px solid rgba(255, 255, 255, 0);
186-
width: 11px;
187-
margin: 0 -5px;
188-
cursor: col-resize;
189-
}
190-
191-
.splitpane_divider_horizontal {
192-
height: 11px;
193-
margin: -5px 0;
194-
border-top: 5px solid rgba(255, 255, 255, 0);
195-
border-bottom: 5px solid rgba(255, 255, 255, 0);
196-
cursor: row-resize;
197-
width: 100%;
198-
}
199-
200-
/*
201-
this shim is used to make sure mousemove events
202-
trigger in all the draggable area of the screen
203-
this is not the case due to hterm's <iframe>
204-
*/
205-
.splitpane_shim {
206-
position: fixed;
207-
top: 0;
208-
left: 0;
209-
right: 0;
210-
bottom: 0;
211-
background: transparent;
212-
}
213-
`}</style>
214-
</div>
215-
);
216-
}
217-
218-
componentWillUnmount() {
219-
// ensure drag end
220-
if (this.dragging) {
221-
this.onDragEnd();
222-
}
223-
}
224-
}
117+
) : null}
118+
</React.Fragment>
119+
);
120+
})}
121+
<div style={{display: dragging ? 'block' : 'none'}} className="splitpane_shim" />
122+
123+
<style jsx>{`
124+
.splitpane_panes {
125+
display: flex;
126+
flex: 1;
127+
outline: none;
128+
position: relative;
129+
width: 100%;
130+
height: 100%;
131+
}
132+
133+
.splitpane_panes_vertical {
134+
flex-direction: row;
135+
}
136+
137+
.splitpane_panes_horizontal {
138+
flex-direction: column;
139+
}
140+
141+
.splitpane_pane {
142+
flex: 1;
143+
outline: none;
144+
position: relative;
145+
}
146+
147+
.splitpane_divider {
148+
box-sizing: border-box;
149+
z-index: 1;
150+
background-clip: padding-box;
151+
flex-shrink: 0;
152+
}
153+
154+
.splitpane_divider_vertical {
155+
border-left: 5px solid rgba(255, 255, 255, 0);
156+
border-right: 5px solid rgba(255, 255, 255, 0);
157+
width: 11px;
158+
margin: 0 -5px;
159+
cursor: col-resize;
160+
}
161+
162+
.splitpane_divider_horizontal {
163+
height: 11px;
164+
margin: -5px 0;
165+
border-top: 5px solid rgba(255, 255, 255, 0);
166+
border-bottom: 5px solid rgba(255, 255, 255, 0);
167+
cursor: row-resize;
168+
width: 100%;
169+
}
170+
171+
/*
172+
this shim is used to make sure mousemove events
173+
trigger in all the draggable area of the screen
174+
this is not the case due to hterm's <iframe>
175+
*/
176+
.splitpane_shim {
177+
position: fixed;
178+
top: 0;
179+
left: 0;
180+
right: 0;
181+
bottom: 0;
182+
background: transparent;
183+
}
184+
`}</style>
185+
</div>
186+
);
187+
});
188+
189+
SplitPane.displayName = 'SplitPane';
190+
191+
export default SplitPane;

typings/hyper.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export type HyperActions = (
192192
import type configureStore from '../lib/store/configure-store';
193193
export type HyperDispatch = ReturnType<typeof configureStore>['dispatch'];
194194

195-
import type {ReactChild} from 'react';
195+
import type {ReactChild, ReactNode} from 'react';
196196
type extensionProps = Partial<{
197197
customChildren: ReactChild | ReactChild[];
198198
customChildrenBefore: ReactChild | ReactChild[];
@@ -264,8 +264,9 @@ export type NotificationProps = {
264264
export type SplitPaneProps = {
265265
borderColor: string;
266266
direction: 'horizontal' | 'vertical';
267-
onResize: Function;
267+
onResize: (sizes: number[]) => void;
268268
sizes?: Immutable<number[]> | null;
269+
children: ReactNode[];
269270
};
270271

271272
import type Term from '../lib/components/term';

0 commit comments

Comments
 (0)