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>
This is unacceptable! Pointer events to the rescue! I suddenly remembered that CSS has a property called pointer-events which allows you to decide whether or not an element should react to pointer events. Pointer events are anything that has to do you your mouse, such as clicking, scrolling, or moving your mouse over (hover) an element.
“The use of pointer-events in CSS for non-SVG elements is experimental. The feature used to be part of the CSS3 UI draft specification but, due to many open issues, has been postponed to CSS4.” — Mozilla MDN
It works, but note that; inside an element where pointer-events have been set to none, for IE11 to take notice of a pointer-events: auto, the target element must also have a CSS position other than static: The only issue I have with it for links is it doesn’t display a link’s Title on hover.
Note that preventing an element from being the target of pointer events by using pointer-events does not necessarily mean that pointer event listeners on that element cannot or will not be triggered.
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>
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With