Skip to content

Instantly share code, notes, and snippets.

@Skitionek
Created November 6, 2017 07:37
Show Gist options
  • Save Skitionek/46c9b7508c48f3c4710b1b3d8ba03acc to your computer and use it in GitHub Desktop.
Save Skitionek/46c9b7508c48f3c4710b1b3d8ba03acc to your computer and use it in GitHub Desktop.
Amazingly simple yet powerful, flex based Split Pane implementation (draggable!)
/**
* Created by Dominik Maszczyk (https://www.linkedin.com/in/dominik-maszczyk/)
* on 30/10/17.
*/
import React, {Component, PureComponent} from "react";
import "./style.css";
class Resizer extends PureComponent {
onDrag = (e) => {
let horizontal = this.props.orientation === "horizontal",
client = horizontal ? e.clientX : e.clientY,
offset = horizontal ? this.resizer.offsetLeft : this.resizer.offsetTop,
delta = client - offset;
if (!client || Math.abs(delta) < 5) return; // avoid jerky montion while hold but not move + limit frequency of update events
this.props.update(delta);
};
onMouseDown = () => {
window.addEventListener("mousemove", this.onMouseMove);
window.addEventListener("mouseup", this.onMouseUp, {passive: true, once: true}); // removed once fired
};
onMouseMove = (e) => {
e.preventDefault();
this.onDrag(e);
};
onMouseUp = () => {
window.removeEventListener("mousemove", this.onMouseMove);
};
render() {
return <div onMouseDown={this.onMouseDown}
ref={(resizer) => this.resizer = resizer}
className="Resizer"/>;
}
}
class SplitPane extends Component {
constructor(props) {
super(props);
this.state = this.calculateState(props);
}
calculateState(props) {
let styles = this.state && this.state.styles || [],
children = [];
props.children.forEach((child, i, iterable) => {
let flex = (props.flex && typeof props.flex[i] === 'number');
let old_flex = (this.props.flex && typeof this.props.flex[i] === 'number');
if(!styles[i] || flex[i]!==old_flex[i]) { // if child has no flex value or flex props update...
styles.push({ // ...update its style with default 1
flexGrow: flex ? props.flex[i] : 1
});
}
children.push(child);
let update = this.update(i);
if (iterable[i + 1]) children.push(<Resizer key={"r" + i} update={update} orientation={this.props.orientation}/>);
});
return {
styles,
children,
horizontal: props.orientation
};
}
update(index) {
return (delta) => {
let styles = this.state.styles,
size = this.state.horizontal ? window.outerWidth : window.outerHeight, // size can be container based instead
step = delta * (this.props.children.length-1) / size; // Δflex ~= Δpx * children.lenght/size (closest empirical approx)
let flex0 = styles[index].flexGrow + step,
flex1 = styles[index + 1].flexGrow - step;
flex0*=flex0>0?1:0; // flex grow should not have value smaller than 0
flex1*=flex1>0?1:0;
styles[index] = {flexGrow: flex0}; //update just neighbour containers, one might want to update all of them depends on desired behaviour
styles[index + 1] = {flexGrow: flex1};
this.setState({
styles
});
}
}
componentWillReceiveProps(props) {
if (this.props.children.length !== props.children.length) {
this.setState(this.calculateState(props));
}
}
render() {
return <div className={`SplitPane ${this.props.orientation}`}>
{this.state.children.map((child, i) => {
if (i % 2 === 0) return <div key={"c" + i} style={this.state.styles[i / 2]}>{child}</div>; // even - child
else return child; // odd - resizer
})}
</div>;
}
}
export default SplitPane;
/**
* Created by Dominik Maszczyk (https://www.linkedin.com/in/dominik-maszczyk/)
* on 30/10/17.
*/
// core styles which makes everything works
.SplitPane {
display: flex;
&>* {
flex: 1 1;
}
.Resizer {
flex: 0 1;
position: relative;
}
&.horizontal {
height: 100%;
flex-direction: row;
}
&.vertical {
height: 100%;
flex-direction: column;
}
}
//example theme
$resizer-border: 4px;
.SplitPane {
.Resizer{
background-color: rgba(0,0,0,0.1);
padding: $resizer-border / 2;
border: $resizer-border solid transparent;
background-clip: padding-box;
transition-property: background-color, border-color;
transition: 0.2s 0.1s;
&:hover {
background-color: rgba(0,0,0,0.2);
border-color: rgba(0,0,0,0.1);
}
}
&.horizontal .Resizer {
cursor: col-resize;
}
&.vertical .Resizer {
cursor: row-resize;
}
}
import SplitPane from "./SplitPane";
// ...
<SplitPane orientation="horizontal" flex={[0.2, 0.8]}> // horizontal split with init values of 20 and 80%
<SplitPane orientation="vertical"> // it can be nested
<div/> // and filled with various number and kinds of elements
<span/>
<p/>
</SplitPane>
<div />
</SplitPane>
//...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment