检测元素外的点击事件(纯JavaScript)

118

我到处寻找一个好的解决方案,但是我找不到任何一个不使用jQuery的方案。

是否有一种跨浏览器的常规方法(无需奇怪的技巧或容易破坏代码),可以检测元素外部的单击事件(可能具有子元素)?


在 body 标签上设置 onclick 事件处理程序是否可以完成任务? - Benjamin Toueg
8个回答

277

document上添加事件监听器,并使用Node.contains()方法来查找事件的目标(即最内层被点击的元素)是否在指定的元素内。它甚至可以在IE5中工作。

const specifiedElement = document.getElementById('a')

// I'm using "click" but it works with any event
document.addEventListener('click', event => {
  const isClickInside = specifiedElement.contains(event.target)

  if (!isClickInside) {
    // The click was OUTSIDE the specifiedElement, do something
  }
})

var specifiedElement = document.getElementById('a');

//I'm using "click" but it works with any event
document.addEventListener('click', function(event) {
  var isClickInside = specifiedElement.contains(event.target);
  if (isClickInside) {
    alert('You clicked inside A')
  } else {
    alert('You clicked outside A')
  }
});
div {
  margin: auto;
  padding: 1em;
  max-width: 6em;
  background: rgba(0, 0, 0, .2);
  text-align: center;
}
Is the click inside A or outside?
<div id="a">A
  <div id="b">B
    <div id="c">C</div>
  </div>
</div>


17
在我看来,这似乎是最优雅的解决方案。 - Marian
7
这是我见过的最佳解决方案。 - HelloWorld

25

你需要在文档级别处理点击事件。在事件对象中,你有一个target属性,它是被点击的最内层DOM元素。通过这个属性,你可以检查它本身并向上遍历其父元素,直到文档元素,以确定其中是否包含你所观察的元素。

请参见jsFiddle上的示例。

document.addEventListener("click", function (e) {
  var level = 0;
  for (var element = e.target; element; element = element.parentNode) {
    if (element.id === 'x') {
      document.getElementById("out").innerHTML = (level ? "inner " : "") + "x clicked";
      return;
    }
    level++;
  }
  document.getElementById("out").innerHTML = "not x clicked";
});

像以往一样,由于addEventListener/attachEvent的原因,这不是跨浏览器兼容的,但它的工作方式如下。

当子元素被点击时,如果不是事件目标,则其某个父级元素是被监视的元素(我仅为此计算级别)。您还可以有一个布尔变量,用于判断是否找到该元素,以便从for循环内部不返回处理程序。我的示例仅限于处理程序仅在没有匹配项时才完成。

为了实现跨浏览器兼容性,我通常会这样做:

var addEvent = function (element, eventName, fn, useCapture) {
  if (element.addEventListener) {
    element.addEventListener(eventName, fn, useCapture);
  }
  else if (element.attachEvent) {
    element.attachEvent(eventName, function (e) {
      fn.apply(element, arguments);
    }, useCapture);
  }
};

这是一个跨浏览器兼容的代码,用于附加事件监听器/处理程序,包括在IE中重写this为元素,就像jQuery为其事件处理程序所做的那样。有许多理由考虑要记住一些jQuery的要点;)


5
如果用户点击了元素的子元素,会怎样? - oxygen
3
使用这个解决方案,您可以检查 target 属性(正如 metadings 所说,它将是最内部的 DOM 元素)以及其所有父元素。如果您到达了要监视的元素,则表示用户在其中单击;如果您到达文档元素,则表示他/她在外部点击。或者,您可以在文档元素和监视元素上添加两个点击侦听器,后者使用 event.stopPropagation() / cancelBubble,以便文档元素上的侦听器仅在用户在监视元素外单击时触发。 - JimmiTh
2
这是一些聪明地使用 for 的方法。出于好奇,不使用严格比较将变量中的元素与事件目标进行比较是否有任何原因? - oxygen
@Tiberiu-IonuțStan 是的,如果您使用引用而不是id 'x'来创建此“watch”,我建议您使用三个等号的element === watchedElement。 - metadings
你的解决方案非常好。这可能是个人问题,但我宁愿分享一下。我有同样的需求,但是当用户点击外部时,一个按钮点击会显示一个div,应该隐藏。这种解决方案的问题是div永远不会再次显示。我在这里分叉了你的代码作为示例:http://jsfiddle.net/bx5hnL2h/ - Alexandre Schmidt

8
这样怎么样:

jsBin演示

document.onclick = function(event){
  var hasParent = false;
    for(var node = event.target; node != document.body; node = node.parentNode)
    {
      if(node.id == 'div1'){
        hasParent = true;
        break;
      }
    }
  if(hasParent)
    alert('inside');
  else
    alert('outside');
} 

2

您可以使用 composedPath() 来检查点击事件是否发生在可能具有子元素的目标 div 内或外:

const targetDiv = document.querySelector('#targetDiv')

document.addEventListener('click', (e) => {
  const isClickedInsideDiv = e.composedPath().includes(targetDiv)

  if (isClickedInsideDiv) {
    console.log('clicked inside of div')
  } else {
    console.log('clicked outside of div')
  } 
})

0
使用 JavaScript 的 Element.closest() 方法:
let popup = document.querySelector('.parent-element')
popup.addEventListener('click', (e) => {
   if (!e.target.closest('.child-element')) {
      // clicked outside
   }
});

我把popup.addEventListener改成了document.addEventListener,然后问题就解决了 - undefined

0

我对它进行了大量的研究,以找到更好的方法。JavaScript 的 .contains 方法会在 DOM 中递归检查是否包含目标。我在一个 React 项目中使用了它,但当 React DOM 在设置状态时发生变化时,.contains 方法不起作用。因此,我想出了这个解决方案。

//Basic Html snippet
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
<div id="mydiv">
  <h2>
    click outside this div to test
  </h2>
  Check click outside 
</div>
</body>
</html>


//Implementation in Vanilla javaScript
const node = document.getElementById('mydiv')
//minor css to make div more obvious
node.style.width = '300px'
node.style.height = '100px'
node.style.background = 'red'

let isCursorInside = false

//Attach mouseover event listener and update in variable
node.addEventListener('mouseover', function() {
  isCursorInside = true
  console.log('cursor inside')
})

/Attach mouseout event listener and update in variable
node.addEventListener('mouseout', function() {
    isCursorInside = false
  console.log('cursor outside')
})


document.addEventListener('click', function() {
  //And if isCursorInside = false it means cursor is outside 
    if(!isCursorInside) {
    alert('Outside div click detected')
  }
})

工作演示 jsfiddle


-2
为了通过单击元素外部来隐藏元素,我通常会应用如下简单代码:

var bodyTag = document.getElementsByTagName('body');
var element = document.getElementById('element'); 
function clickedOrNot(e) {
 if (e.target !== element) {
  // action in the case of click outside 
  bodyTag[0].removeEventListener('click', clickedOrNot, true);
 } 
}
bodyTag[0].addEventListener('click', clickedOrNot, true);


-2

另一个非常简单和快速的方法是将path数组映射到监听器返回的事件对象中。如果您元素的idclass名称与数组中的某个名称匹配,则单击在您的元素内。

(如果您不想直接获取元素(例如:document.getElementById('...')),则此解决方案可能很有用,例如在reactjs / nextjs应用程序中,在ssr中..)。

这里是一个例子:

   document.addEventListener('click', e => {
      let clickedOutside = true;

      e.path.forEach(item => {
        if (!clickedOutside)
          return;

        if (item.className === 'your-element-class')
          clickedOutside = false;
      });

      if (clickedOutside)
        // Make an action if it's clicked outside..
    });

我希望这个答案能对你有所帮助!(如果我的解决方案不好或者你有什么改进意见,请告诉我。)


1
嵌套元素怎么办? - oxygen
@Tiberiu-IonuțStan,您可以访问整个节点列表,从父节点到子节点,因此您可以轻松地对它们进行操作。 - hpapier
1
突然间,你的解决方案不再简单了。 - oxygen
@Tiberiu-IonuțStan 为什么?就个人而言,我认为这个解决方案在React或复杂的JS环境中是最简单的。不需要任何引用,也不需要如果DOM需要时间加载才能实现解决方案,你有更多的控制权。 - hpapier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接