let appStyles = {
original: {
background: 'linear-gradient(red, yellow)',
height: '1600px',
},
modified: {
background: 'linear-gradient(blue, green)',
height: '2100px',
},
};
let Pane = React.forwardRef((props, ref) => {
return <PaneComponent {...props} forwardedRef={ref} />;
});
let PaneWithScrollSync = withScrollSync(Pane);
class App extends React.Component {
render() {
return (
<div className="app">
<FakeScrollBar scrollHeight={2100}>
<Splitter>
<PaneWithScrollSync>
<pre className="code" style={appStyles.original}>
<code>Content with height: 1600px</code>
</pre>
</PaneWithScrollSync>
<PaneWithScrollSync>
<pre className="code" style={appStyles.modified}>
<code>Ccontent with height: 2100px</code>
</pre>
</PaneWithScrollSync>
</Splitter>
</FakeScrollBar>
</div>
);
}
}
let scrollStyles = {
container: {
display: 'flex',
flexDirection: 'row',
flex: 1,
},
scrollTrack: {
width: 30,
borderLeft: '1px solid',
borderLeftColor: '#000',
background: '#212121',
position: 'relative',
},
scrollThumb: {
position: 'absolute',
background: 'red',
width: '100%',
},
scrollThumbHover: {
background: 'blue',
},
};
const ScrollContext = React.createContext();
class FakeScrollBar extends React.Component {
state = {
scrollTopRelative: 0,
thumbRelativeHeight: 0,
thumbMouseOver: false,
};
constructor(props) {
super(props);
this.scrollTrack = React.createRef();
}
get trackPosition() {
if (!this.scrollTrack.current) {
return 0;
}
return (this.scrollTop / this.props.scrollHeight) * this.scrollTrack.current.clientHeight;
}
get scrollTop() {
return this.state.scrollTopRelative * this.scrollTopMax;
}
get scrollTopMax() {
return this.props.scrollHeight - this.scrollTrack.current.clientHeight;
}
get thumbHeight() {
if (!this.scrollTrack.current) {
return 0;
}
return this.state.thumbRelativeHeight * this.scrollTrack.current.clientHeight;
}
handleWheel = e => {
if (e.deltaMode !== 0) {
console.error('The scrolling is not in pixel mode!');
return false;
}
let deltaYPercentage = e.deltaY / this.scrollTopMax;
let scrollTopRelative = Math.min(
Math.max(this.state.scrollTopRelative + deltaYPercentage, 0),
1
);
this.setState({
scrollTopRelative,
});
};
handleMouseEnterThumb = e => {
this.setState({ thumbMouseOver: true });
};
handleMouseLeaveThumb = e => {
this.setState({ thumbMouseOver: false });
};
getSyncedPosition = container => {};
componentDidMount() {
this.updateScrollThumbHeight();
window.addEventListener('resize', this.updateScrollThumbHeight);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateScrollThumbHeight);
}
updateScrollThumbHeight = e => {
this.setState({
thumbRelativeHeight: this.scrollTrack.current.clientHeight / this.props.scrollHeight,
});
};
render() {
let { thumbMouseOver } = this.state;
return (
<ScrollContext.Provider value={this.state}>
<div style={scrollStyles.container} onWheel={this.handleWheel}>
{this.props.children}
<div ref={this.scrollTrack} style={scrollStyles.scrollTrack}>
<div
onMouseEnter={this.handleMouseEnterThumb}
onMouseLeave={this.handleMouseLeaveThumb}
style={Object.assign(
{ top: this.trackPosition },
{ height: this.thumbHeight },
scrollStyles.scrollThumb,
thumbMouseOver && scrollStyles.scrollThumbHover
)}
/>
</div>
</div>
</ScrollContext.Provider>
);
}
}
let splitterStyles = {
container: {
display: 'flex',
flexDirection: 'row',
flex: 1,
},
bridge: {
width: '40px',
height: '100vh',
position: 'relative',
background: 'linear-gradient(white, black)',
cursor: 'ew-resize',
},
};
class Splitter extends React.Component {
state = {
dragging: false,
leftPaneFlex: 0.5,
rightPaneFlex: 0.5,
};
componentDidMount() {
if (this.props.children.length !== 2) {
console.error('The splitter needs to `Pane` children to work');
}
}
handleMouseUp = e => {
this.setState({ dragging: false });
this.bridge.removeEventListener('mouseup', this.handleMouseUp);
};
handleMouseMove = e => {
if (!this.state.dragging) {
return;
}
let splitterPosition = this.getRelativeContainerX(e.clientX);
console.log(splitterPosition);
this.setState({
leftPaneFlex: splitterPosition,
rightPaneFlex: 1 - splitterPosition,
});
};
handleMouseDown = e => {
this.setState({ dragging: true });
document.addEventListener('mouseup', this.handleMouseUp);
document.addEventListener('mousemove', this.handleMouseMove);
};
getRelativeContainerX(x) {
var rect = this.container.getBoundingClientRect();
return (x - rect.left) / rect.width;
}
render() {
const { children } = this.props;
let commonProps = {
dragging: this.state.dragging,
};
const leftPane = React.cloneElement(children[0], {
...commonProps,
flex: this.state.leftPaneFlex,
});
const rightPane = React.cloneElement(children[1], {
...commonProps,
flex: this.state.rightPaneFlex,
});
return (
<div style={splitterStyles.container} ref={container => (this.container = container)}>
{leftPane}
<div
style={{ ...splitterStyles.bridge }}
ref={bridge => (this.bridge = bridge)}
onDrag={this.handleDrag}
onMouseDown={this.handleMouseDown}
/>
{rightPane}
</div>
);
}
}
let paneStyles = {
scrollContainer: {
height: '100vh',
overflow: 'hidden',
},
pane: {
flex: 1,
minWidth: 'fit-content',
border: '5px solid', // remove
borderColor: 'cyan', // remove
},
};
class PaneComponent extends React.Component {
render() {
const { forwardedRef, dragging, ...rest } = this.props;
return (
<div
ref={forwardedRef}
style={{ flex: this.props.flex, ...paneStyles.scrollContainer }}
{...rest}
>
<div
style={{
userSelect: dragging ? 'none' : 'auto',
...paneStyles.pane,
}}
>
{this.props.children}
</div>
</div>
);
}
}
function withScrollSync(WrappedComponent) {
class ScrollSynced extends React.Component {
constructor(props) {
super(props);
this.wrappedComponent = React.createRef();
}
componentDidUpdate() {
let { scrollTopRelative } = this.props;
if (!this.wrappedComponent) {
return;
}
let { scrollHeight, clientHeight } = this.wrappedComponent.current;
this.wrappedComponent.current.scrollTop = (scrollHeight - clientHeight) * scrollTopRelative;
}
render() {
let { scrollTopRelative, forwardedRef, ...rest } = this.props;
return <WrappedComponent ref={this.wrappedComponent} {...rest} />;
}
}
ScrollSynced.propTypes = WrappedComponent.propTypes;
return React.forwardRef((props, ref) => (
<ScrollContext.Consumer>
{state => (
<ScrollSynced
{...props}
forwardedRef={ref}
scrollTopRelative={state.scrollTopRelative}
/>
)}
</ScrollContext.Consumer>
));
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
body {
margin: 0;
overflow-y: hidden;
}
.app {
display: flex;
flex-direction: row;
}
.code {
margin: 0;
}
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root">
</div>