禁用除覆盖 div 上的滚动之外的所有指针事件

mic*_*nil 6 html javascript css

问题:

我有两个带有溢出文本内容的容器,如下所示: 两个不可滚动的文本容器

蓝色<div>的地方overflow:hidden。现在我想以自定义的同步 * 方式滚动这些 div,而不管<div>我在白色容器中滚动的位置。我的想法是我可以创建一个绝对定位透明<div>作为白色容器的直接子级,并给它一个溢出子级:

可滚动覆盖

其中蓝色容器的 z-index 高于原始两个文本容器:

.container {
  width: 100vw;
  height: 100vh;
  z-index: 10;
  position: absolute;
  overflow-y: scroll;
}
Run Code Online (Sandbox Code Playgroud)

所以最终的结果是这样的:

最终叠加

现在我希望能够滚动覆盖容器,但在底层元素中捕获其他鼠标事件(如文本选择)

我的目标是在滚动覆盖容器时使用 JavaScript 手动滚动底层容器。

题:

鉴于无法通过css 属性pointer-events有选择地禁用指针事件,有没有其他方法可以仅启用覆盖元素的滚动事件,同时将其他指针事件传递给底层元素?

背景:

*我正在尝试的实现类似于 Perforce P4Merge 使用他们的 diff 工具所做的。它们有一个用于 2 个代码块的垂直滚动条,我假设滚动高度大于两个代码块中的任何一个。在某些情况下,滚动事件会滚动两个代码块,有时只是其中一个,而在其他情况下,它们会以不同的速度滚动(取决于添加和删除的内容)。

更新: 原来的实现是写在反应,在该代码我不必有margin-left: -18px;scrollable-container显示滚动条。不知道为什么。此外,如果您愿意,这里有一个 codepen:codepen 片段

.container {
  width: 100vw;
  height: 100vh;
  z-index: 10;
  position: absolute;
  overflow-y: scroll;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)

mic*_*nil 1

现在已经过去了几天了,根据我的研究,似乎不可能以这种方式实现我想要的目标。不可能有选择地禁用指针事件,而且我找不到任何解决方法。

相反,我能想到的最好的方法是实现我自己的“假”滚动条。此滚动条实现订阅wheel容器的事件,然后我同步子滚动容器以具有相同的位置。我暂时不会接受这个问题,以防有人针对我的要求提出更好的解决方案。

对于任何感兴趣的人,您都可以在下面找到我的解决方案。 注意:选择整页视图以获得更好的体验。

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')
)
Run Code Online (Sandbox Code Playgroud)
body {
  margin: 0;
	overflow-y: hidden;
}

.app {
  display: flex;
  flex-direction: row;
}

.code {
  margin: 0;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)