Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable all pointer events except scroll on overlaying div

Problem:

I have two containers with overflowing text content like so: two non-scrollable text containers

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:

scrollable overlay

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:

final overlay

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>
like image 719
micnil Avatar asked May 13 '18 10:05

micnil


People also ask

What are pointer events in CSS?

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.

Can you use pointer events in CSS for non SVG elements?

“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

Does the auto pointer event work in IE11?

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.

Can an element be the target of a pointer event?

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.


2 Answers

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>
like image 55
Amirul Asraf Avatar answered Nov 07 '22 14:11

Amirul Asraf


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>
like image 33
micnil Avatar answered Nov 07 '22 16:11

micnil