我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。jQueryclosest()用于查看单击事件的目标是否将dom元素作为其父元素之一。如果存在匹配项,则单击事件属于其中一个子项,因此不被视为在组件之外。

因此,在我的组件中,我想将一个单击处理程序附加到窗口。当处理程序启动时,我需要将目标与组件的dom子级进行比较。

click事件包含类似“path”的财产,它似乎保存了事件经过的dom路径。我不知道该比较什么,或者如何最好地遍历它,我想肯定有人已经把它放在了一个聪明的效用函数中。。。不


当前回答

我喜欢@Ben Bud的答案,但当有视觉上嵌套的元素时,contains(event.target)并不能像预期的那样工作。

因此,有时最好计算点击点是否位于元素内部。

这是我的React Hook代码。

import { useEffect } from 'react'

export function useOnClickRectOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      const targetEl = ref.current
      if (targetEl) {
        const clickedX = event.clientX
        const clickedY = event.clientY
        const rect = targetEl.getBoundingClientRect()
        const targetElTop = rect.top
        const targetElBottom = rect.top + rect.height
        const targetElLeft = rect.left
        const targetElRight = rect.left + rect.width

        if (
          // check X Coordinate
          targetElLeft < clickedX &&
          clickedX < targetElRight &&
          // check Y Coordinate
          targetElTop < clickedY &&
          clickedY < targetElBottom
        ) {
          return
        }

        // trigger event when the clickedX,Y is outside of the targetEl
        handler(event)
      }
    }

    document.addEventListener('mousedown', listener)
    document.addEventListener('touchstart', listener)

    return () => {
      document.removeEventListener('mousedown', listener)
      document.removeEventListener('touchstart', listener)
    }
  }, [ref, handler])
}

其他回答

我喜欢@Ben Bud的答案,但当有视觉上嵌套的元素时,contains(event.target)并不能像预期的那样工作。

因此,有时最好计算点击点是否位于元素内部。

这是我的React Hook代码。

import { useEffect } from 'react'

export function useOnClickRectOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      const targetEl = ref.current
      if (targetEl) {
        const clickedX = event.clientX
        const clickedY = event.clientY
        const rect = targetEl.getBoundingClientRect()
        const targetElTop = rect.top
        const targetElBottom = rect.top + rect.height
        const targetElLeft = rect.left
        const targetElRight = rect.left + rect.width

        if (
          // check X Coordinate
          targetElLeft < clickedX &&
          clickedX < targetElRight &&
          // check Y Coordinate
          targetElTop < clickedY &&
          clickedY < targetElBottom
        ) {
          return
        }

        // trigger event when the clickedX,Y is outside of the targetEl
        handler(event)
      }
    }

    document.addEventListener('mousedown', listener)
    document.addEventListener('touchstart', listener)

    return () => {
      document.removeEventListener('mousedown', listener)
      document.removeEventListener('touchstart', listener)
    }
  }, [ref, handler])
}

您只需在主体上安装一个双击处理程序,并在此元素上安装另一个。在该元素的处理程序中,只需返回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>

所以我遇到了一个类似的问题,但在我的案例中,这里选择的答案不起作用,因为我有一个下拉菜单按钮,这是文档的一部分。因此,单击该按钮也会触发handleClickOutside函数。为了防止触发,我必须向按钮和这个添加一个新的引用!menuBtnRef.current.contents(e.target)设置为条件。如果有人像我一样面临同样的问题,我就把它留在这里。

下面是组件现在的样子:


const Component = () => {

    const [isDropdownOpen, setIsDropdownOpen] = useState(false);
    const menuRef     = useRef(null);
    const menuBtnRef  = useRef(null);

    const handleDropdown = (e) => {
        setIsDropdownOpen(!isDropdownOpen);
    }

    const handleClickOutside = (e) => {
        if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {
            setIsDropdownOpen(false);
        }
    }

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside, true);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside, true);
        };
    }, []);

    return (

           <button ref={menuBtnRef} onClick={handleDropdown}></button>

           <div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>
                // ...dropdown items
           </div>
    )
}

我为所有场合制定了解决方案。

你应该使用一个高阶组件来包装你想要监听的组件。

这个组件示例只有一个属性:“onClickedOutside”,它接收函数。

ClickedOutside.js
import React, { Component } from "react";

export default class ClickedOutside extends Component {
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  handleClickOutside = event => {
    // IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.props.onClickedOutside();
    }
  };

  render() {
    // In this piece of code I'm trying to get to the first not functional component
    // Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
    let firstNotFunctionalComponent = this.props.children;
    while (typeof firstNotFunctionalComponent.type === "function") {
      firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
    }

    // Here I'm cloning the element because I have to pass a new prop, the "reference" 
    const children = React.cloneElement(firstNotFunctionalComponent, {
      ref: node => {
        this.wrapperRef = node;
      },
      // Keeping all the old props with the new element
      ...firstNotFunctionalComponent.props
    });

    return <React.Fragment>{children}</React.Fragment>;
  }
}

https://stackoverflow.com/a/42234988/9536897解决方案在手机上不起作用。

您可以尝试:

  // returns true if the element or one of its parents has the class classname
  hasSomeParentTheClass(element, classname) {
    if(element.target)
    element=element.target;
    
    if (element.className&& element.className.split(" ").indexOf(classname) >= 0) return true;
    return (
      element.parentNode &&
      this.hasSomeParentTheClass(element.parentNode, classname)
    );
  }
  componentDidMount() {
    const fthis = this;

    $(window).click(function (element) {
      if (!fthis.hasSomeParentTheClass(element, "myClass"))
        fthis.setState({ pharmacyFocus: null });
    });
  }

在视图中,将className赋给特定元素。