我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。
因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。
click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不
所有提出的解决方案都假设可以将事件添加到文档中,并依赖于本机方法.contains()来区分事件是在当前组件内部还是外部触发的
ref.current.contains(event.target)
但这在React中并不总是有效的。事实上,在React中,React.createPortal API允许从一个组件中指定一个新的真实父组件,JSX在该组件中被呈现,但同时,事件冒泡被模拟为组件在声明的位置呈现(即React.cCreatePortal被调用的位置)。
这是通过将所有事件附加到应用程序根元素并在Javascript中模拟事件来实现的。
因此,在这种情况下,所提出的解决方案被打破了,因为在门户元素内部的单击,对于标准HTML来说,在当前元素之外,实际上应该像在内部一样进行处理。
所以我重写了这个问题的一个注释中提出的解决方案,并对其进行了重构以使用功能组件。这也适用于一个或多个嵌套门户。
export default function OutsideClickDetector({onOutsideClick, Component ='div', ...props} : OutsideClickDetectorProps) {
const isClickInside = useRef<boolean>(false);
const onMouseDown = () => {
isClickInside.current = true;
};
const handleBodyClick = useCallback((e) => {
if(!isClickInside.current) {
onOutsideClick?.(e);
}
isClickInside.current = false;
}, [isClickInside, onOutsideClick]);
useEffect(() => {
document.addEventListener('mousedown', handleBodyClick);
return () => document.removeEventListener('mousedown', handleBodyClick);
});
return <Component onMouseDown={onMouseDown} onMouseUp={() => isClickInside.current = false}{...props} />;
}
将onClick处理程序添加到顶级容器中,并在用户单击时增加状态值。将该值传递给相关组件,每当该值发生变化时,您就可以执行操作。
在本例中,每当clickCount值更改时,我们都会调用this.closeDropdown()。
incrementClickCount方法在.app容器中激发,而不是在下拉列表中激发,因为我们使用event.stopPropagation()来防止事件冒泡。
您的代码可能最终看起来像这样:
class App extends Component {
constructor(props) {
super(props);
this.state = {
clickCount: 0
};
}
incrementClickCount = () => {
this.setState({
clickCount: this.state.clickCount + 1
});
}
render() {
return (
<div className="app" onClick={this.incrementClickCount}>
<Dropdown clickCount={this.state.clickCount}/>
</div>
);
}
}
class Dropdown extends Component {
constructor(props) {
super(props);
this.state = {
open: false
};
}
componentDidUpdate(prevProps) {
if (this.props.clickCount !== prevProps.clickCount) {
this.closeDropdown();
}
}
toggleDropdown = event => {
event.stopPropagation();
return (this.state.open) ? this.closeDropdown() : this.openDropdown();
}
render() {
return (
<div className="dropdown" onClick={this.toggleDropdown}>
...
</div>
);
}
}
您只需在主体上安装一个双击处理程序,并在此元素上安装另一个。在该元素的处理程序中,只需返回false以防止事件传播。因此,当双击发生时,如果它在元素上,它将被捕获,并且不会传播到主体上的处理程序。否则它会被身体上的处理程序抓住。
更新:如果你真的不想阻止事件传播,你只需要使用closest来检查点击是发生在你的元素还是他的一个孩子身上:
<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
$(document).on('click', function(event) {
if (!$(event.target).closest('#div3').length) {
alert("outside");
}
});
</script>
</head>
<body>
<div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
<div style="background-color:red;width:100px;height:100px;" id="div2"></div>
<div style="background-color:green;width:100px;height:100px;" id="div3"></div>
<div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
<div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>
更新:不带jQuery:
<html>
<head>
<script>
function findClosest (element, fn) {
if (!element) return undefined;
return fn(element) ? element : findClosest(element.parentElement, fn);
}
document.addEventListener("click", function(event) {
var target = findClosest(event.target, function(el) {
return el.id == 'div3'
});
if (!target) {
alert("outside");
}
}, false);
</script>
</head>
<body>
<div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
<div style="background-color:red;width:100px;height:100px;" id="div2"></div>
<div style="background-color:green;width:100px;height:100px;" id="div3">
<div style="background-color:pink;width:50px;height:50px;" id="div6"></div>
</div>
<div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
<div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>
我在discuss.reactjs.org上找到了一个解决方案,这要感谢Ben Alpert。单击我的树中的一个组件会导致一个重新阅读程序,在更新时删除了单击的元素。由于React的重读发生在调用文档正文处理程序之前,因此元素未被检测为“在”树中。
解决方案是在应用程序根元素上添加处理程序。
主要:
window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)
组件:
import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
export default class ClickListener extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
onClickOutside: PropTypes.func.isRequired
}
componentDidMount () {
window.__myapp_container.addEventListener('click', this.handleDocumentClick)
}
componentWillUnmount () {
window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
}
/* using fat arrow to bind to instance */
handleDocumentClick = (evt) => {
const area = ReactDOM.findDOMNode(this.refs.area);
if (!area.contains(evt.target)) {
this.props.onClickOutside(evt)
}
}
render () {
return (
<div ref='area'>
{this.props.children}
</div>
)
}
}