Reputation: 4805
Problem:
I have two containers with overflowing text content like so:
where the blue <div>
s have overflow:hidden
. Now I want to scroll these divs in a customized synchronized* way regardless of where in the white container <div>
i scroll. My thinking was that I could create a absolutely positioned transparent <div>
as a direct child to the white container, and give it a overflowing child:
where the blue container has a higher z-index than the original two text containers:
.container {
width: 100vw;
height: 100vh;
z-index: 10;
position: absolute;
overflow-y: scroll;
}
So that the final result looks something like this:
Now I want to be able to scroll the overlaying container but capture other mouse events (like text selection) in the underlaying elements.
My goal is to manually scroll the underlaying containers with JavaScript, when the overlaying container is scrolled.
Question:
Given that there is no way to selectively disable pointer-event with the css property pointer-events
, is there any other way to enable only the scroll event of the overlaying element while passing other pointer events to the underlaying elements?
Background:
*What I am trying the achieve is similar to what Perforce P4Merge has done with thier diff tool. They have one vertical scrollbar for 2 code blocks, where I assume the scroll height is larger than either of the two code blocks. In some cases the scroll event will scroll both code blocks, sometimes just one of them, and in other cases they scroll with different speeds (depending on added and removed content).
Update:
Original implementation is written in react, and in that code I dont have to have margin-left: -18px;
on scrollable-container
to show the scrollbar. dont know why. Also, here is a codepen if you prefer: codepen snippet
body {
overflow-y: hidden;
}
.app {
overflow-y: hidden;
position: relative;
display: flex;
flex-direction: row;
z-index: 0;
}
.scrollable-container {
width: 100vw;
height: 100vh;
z-index: 10;
margin-left: -18px;
position: absolute;
overflow-y: scroll;
}
.scrollable-content {
width: 500px;
height: 1600px;
}
.non-scrollable-container {
flex: 1;
height: 100vh;
overflow-y: hidden;
}
.bridge {
width: 40px;
background: linear-gradient(white, black);
cursor: ew-resize;
height: 100vh;
}
#original {
background: linear-gradient(red, yellow);
height: 2100px;
}
#modified {
background: linear-gradient(blue, green);
height: 1600px;
}
<div class="app">
<div class="scrollable-container">
<div class="scrollable-content"></div>
</div>
<div class="non-scrollable-container">
<div id="original" class="codeBlock">
Content I want to select
</div>
</div>
<div class="bridge"></div>
<div class="non-scrollable-container">
<div id="modified" class="codeBlock">
Content I want to select
</div>
</div>
</div>
Upvotes: 8
Views: 12544
Reputation: 41
The question is quite old but in case if anyone looking for similar thing here are the solution i found. I used java script event listener to temporarily disable pointer-event on mousedown and re enable the pointer event on mouse up of its parent
function addlistener() {
var scrollable = document.getElementsByClassName("scrollable-container")[0];
scrollable.addEventListener('mousedown', function() {
this.style.pointerEvents = "none";
document.elementFromPoint(event.clientX, event.clientY).click();
}, false);
document.getElementsByClassName("app")[0].addEventListener('mouseup', function(e) {
scrollable.style.pointerEvents = "all";
}, false);
}
body {
overflow-y: hidden;
}
.app {
overflow-y: hidden;
position: relative;
display: flex;
flex-direction: row;
z-index: 0;
}
.scrollable-container {
width: 100vw;
height: 100vh;
margin-left: -18px;
position: absolute;
overflow-y: scroll;
z-index: 10;
}
.scrollable-content {
width: 500px;
height: 1600px;
}
.non-scrollable-container {
flex: 1;
height: 100vh;
overflow-y: hidden;
}
.bridge {
width: 40px;
background: linear-gradient(white, black);
cursor: ew-resize;
height: 100vh;
}
#original {
background: linear-gradient(red, yellow);
height: 2100px;
}
#modified {
background: linear-gradient(blue, green);
height: 1600px;
}
<body onload="addlistener()">
<div class="app">
<div class="scrollable-container">
<div class="scrollable-content"></div>
</div>
<div class="non-scrollable-container">
<div id="original" class="codeBlock">
Content I want to select
</div>
</div>
<div class="bridge"></div>
<div class="non-scrollable-container">
<div id="modified" class="codeBlock">
Content I want to select
</div>
</div>
</div>
</body>
Upvotes: 4
Reputation: 4805
It has now gone a couple of days, and from my research it doesn't seem to be possible to achieve what I want in this way. It is not possible to selectively disable pointer events and I can not find any way around it.
Instead, the best approach I could think of was to implement my own "fake" scrollbar. This scrollbar implementation subscribes to the wheel
event of the container, and then i've synced the child scroll containers to have the same position. I'll leave this question without an accepted answer for now, in case anyone comes up with a better solution for what I asked.
For anyone intrested, you'll find my solution below. Note: select Full Page view for a better experience.
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>
Upvotes: 1