如何检测元素外的单击事件?

2925

我有一些HTML菜单,当用户点击这些菜单的标题时,会完全显示它们。当用户在菜单区域外点击时,我希望隐藏这些元素。

使用jQuery是否可以实现这样的效果?

$("#menuscontainer").clickOutsideThisElement(function() {
    // Hide the menus
});

51
这是该策略的一个示例:http://jsfiddle.net/tedp/aL7Xe/1/ - Ted
22
正如Tom所提到的,您需要在使用此方法之前阅读http://css-tricks.com/dangers-stopping-event-propagation/。那个jsfiddle工具非常酷。 - Jon Coombs
3
获取元素的引用,然后使用 event.target,最后对它们进行 != 或 == 的比较,根据比较结果执行相应的代码。 - Rohit Kumar
尝试使用 event.path。https://dev59.com/XnVC5IYBdhLWcg3w4Vf6#43405204 - Dan Philip Bejoy
9
使用event.target实现的原生JavaScript解决方案,不包含event.stopPropagation - lowtechsun
显示剩余2条评论
91个回答

2011

注意:使用stopPropagation应该避免,因为它会打破DOM中正常的事件流程。有关更多信息,请参见这篇CSS Tricks文章。考虑改用这种方法

在文档主体上附加一个点击事件以关闭窗口。将另一个点击事件附加到容器,以阻止事件传播到文档主体。

$(window).click(function() {
  //Hide the menus if visible
});

$('#menucontainer').click(function(event){
  event.stopPropagation();
});

811
这破坏了许多东西的标准行为,包括 #menucontainer 内的按钮和链接。我很惊讶这个答案如此受欢迎。 - Art
83
这不会改变#menucontainer内部任何东西的行为,因为它在内部任何元素的传播链底部。 - Eran Galperin
102
很美,但你应该使用 $('html').click() 而不是 body。body 的高度始终为其内容的高度。如果内容很少或屏幕很高,则仅在被 body 填充的部分起作用。 - meo
114
我也很惊讶这个解决方案得到了这么多的投票。对于具有stopPropagation属性的外部元素,该解决方案会失败。http://jsfiddle.net/Flandre/vaNFw/3/ - Andre
145
Philip Walton详细解释了为什么这个答案不是最佳解决方案:http://css-tricks.com/dangers-stopping-event-propagation/ - Tom
显示剩余6条评论

1616
你可以在 document 上监听一个 click 事件,然后使用 .closest() 确保被点击的元素不是 #menucontainer 的祖先或目标。

如果不是,则被点击的元素位于 #menucontainer 外部,你可以安全地隐藏它。

$(document).click(function(event) { 
  var $target = $(event.target);
  if(!$target.closest('#menucontainer').length && 
  $('#menucontainer').is(":visible")) {
    $('#menucontainer').hide();
  }        
});

Edit – 2017-06-23

如果您计划关闭菜单并停止监听事件,您还可以在事件监听器后进行清理。该函数仅清理新创建的监听器,保留document上任何其他点击监听器。使用 ES2015 语法:

export function hideOnClickOutside(selector) {
  const outsideClickListener = (event) => {
    const $target = $(event.target);
    if (!$target.closest(selector).length && $(selector).is(':visible')) {
        $(selector).hide();
        removeClickListener();
    }
  }

  const removeClickListener = () => {
    document.removeEventListener('click', outsideClickListener);
  }

  document.addEventListener('click', outsideClickListener);
}

编辑 - 2018-03-11

对于那些不想使用jQuery的人,这里是上面代码使用纯vanillaJS(ECMAScript6)的版本。

function hideOnClickOutside(element) {
    const outsideClickListener = event => {
        if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
          element.style.display = 'none';
          removeClickListener();
        }
    }

    const removeClickListener = () => {
        document.removeEventListener('click', outsideClickListener);
    }

    document.addEventListener('click', outsideClickListener);
}

const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js 

注意: 这是基于Alex的评论而建议使用!element.contains(event.target),而不是jQuery部分。

但是,在所有主要浏览器中现在也可以使用element.closest()(W3C版本与jQuery有些不同)。 可以在此处找到Polyfills: Element.closest()

编辑-2020年05月21日

如果您希望用户能够在元素内单击并拖动鼠标,然后将其释放到元素外而不关闭该元素:

      ...
      let lastMouseDownX = 0;
      let lastMouseDownY = 0;
      let lastMouseDownWasOutside = false;

      const mouseDownListener = (event: MouseEvent) => {
        lastMouseDownX = event.offsetX;
        lastMouseDownY = event.offsetY;
        lastMouseDownWasOutside = !$(event.target).closest(element).length;
      }
      document.addEventListener('mousedown', mouseDownListener);

outsideClickListener中:

const outsideClickListener = event => {
        const deltaX = event.offsetX - lastMouseDownX;
        const deltaY = event.offsetY - lastMouseDownY;
        const distSq = (deltaX * deltaX) + (deltaY * deltaY);
        const isDrag = distSq > 3;
        const isDragException = isDrag && !lastMouseDownWasOutside;

        if (!element.contains(event.target) && isVisible(element) && !isDragException) { // or use: event.target.closest(selector) === null
          element.style.display = 'none';
          removeClickListener();
          document.removeEventListener('mousedown', mouseDownListener); // Or add this line to removeClickListener()
        }
    }

34
我尝试了很多其他答案,但只有这个起作用了。谢谢。我最终使用的代码是:$(document).click( function(event) { if( $(event.target).closest('.window').length == 0 ) { $('.window').fadeOut('fast'); } } ); - Pistos
42
我最终选择了这个解决方案,因为它更好地支持同一页上的多个菜单,而在第一个菜单打开时点击第二个菜单将使第一个菜单保持打开状态,这是在stopPropagation方案中不具备的。 - umassthrower
15
优秀的回答。当您有多个想要关闭的项目时,这是正确的做法。 - John
32
使用Node.contains()替代!element.contains(event.target)实现无需jQuery。 - Alex Ross
5
如果你正在阅读这篇文章,那么你可能应该看看一些更现代的答案来解决这个问题,这些答案比这篇答案更易读。请点击此处查看。 - maxshuty
谢谢。作为用户,我总是希望能够在元素内部单击并拖动,然后在元素外释放鼠标,而不关闭元素。 - Michael

425
如何检测元素外的单击事件?
这个问题之所以如此受欢迎,而且有这么多答案,是因为它表面上很简单,实际上却很复杂。近八年来,数十个答案中我惊讶地发现,对于可访问性方面关注的不够。
我希望当用户单击菜单区域之外时隐藏这些元素。
这是一个高尚的目标,并且是实际问题。问题标题——大多数答案似乎试图解决的问题——包含了一个不幸的红色线索。
提示:就是“单击”这个词!
你实际上不需要绑定单击事件处理程序。
如果你正在绑定单击事件处理程序来关闭对话框,那么你已经失败了。你失败的原因是并不是所有人都会触发单击事件。不使用鼠标的用户可以通过按Tab键来退出对话框(你的弹出菜单也可以被认为是一种对话框),然后他们将无法阅读对话框后面的内容,除非随后触发一个单击事件。
因此,让我们重新说一下这个问题。

当用户完成对话框操作后,该如何关闭对话框?

这是我们的目标。不幸的是,现在我们需要绑定userisfinishedwiththedialog事件,而这个绑定并不那么简单。

那么,我们如何检测用户何时完成对话框操作呢?

focusout事件

一个好的起点是判断焦点是否离开了对话框。

提示:要小心blur事件,如果事件绑定到冒泡阶段,则blur不会传播!

jQuery的focusout非常适合。如果您不能使用jQuery,则可以在捕获阶段使用blur

element.addEventListener('blur', ..., true);
//                       use capture: ^^^^

此外,对于许多对话框,您需要允许容器获得焦点。添加 tabindex="-1" 可以使对话框动态地接收焦点,而不会中断 Tab 键流程。

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on('focusout', function () {
  $(this).removeClass('active');
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>


如果您使用该演示超过一分钟,您应该很快开始看到问题。
首先是对话框中的链接无法点击。尝试单击或切换到它将导致交互发生之前对话框关闭。这是因为聚焦内部元素会在再次触发focusin事件之前触发focusout事件。
解决方法是在事件循环中排队状态更改。这可以通过使用setImmediate(...)或对于不支持setImmediate的浏览器使用setTimeout(..., 0)来完成。一旦排队,它就可以被后续的focusin取消:
$('.submenu').on({
  focusout: function (e) {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function (e) {
    clearTimeout($(this).data('submenuTimer'));
  }
});

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>

第二个问题是当再次点击链接时,对话框不会关闭。这是因为对话框失去焦点触发了关闭行为,之后链接点击触发对话框重新打开。
类似于前一个问题,需要管理焦点状态。考虑到状态变化已经排队,只需处理对话框触发器上的焦点事件即可: 这应该很熟悉
$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>


Esc

如果你认为通过处理焦点状态就完成了所有工作,其实还有更多可以做来简化用户体验。

这通常是一个“附加功能”,但在弹出框或模态框中,按下Esc键通常会将其关闭。

keydown: function (e) {
  if (e.which === 27) {
    $(this).removeClass('active');
    e.preventDefault();
  }
}

$('a').on('click', function () {
  $(this.hash).toggleClass('active').focus();
});

$('div').on({
  focusout: function () {
    $(this).data('timer', setTimeout(function () {
      $(this).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('timer'));
  },
  keydown: function (e) {
    if (e.which === 27) {
      $(this).removeClass('active');
      e.preventDefault();
    }
  }
});

$('a').on({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});
div {
  display: none;
}
.active {
  display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#example">Example</a>
<div id="example" tabindex="-1">
  Lorem ipsum <a href="http://example.com">dolor</a> sit amet.
</div>


如果您知道对话框内有可聚焦的元素,则不需要直接聚焦对话框。如果您正在构建菜单,可以聚焦第一个菜单项。
click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}

$('.menu__link').on({
  click: function (e) {
    $(this.hash)
      .toggleClass('submenu--active')
      .find('a:first')
      .focus();
    e.preventDefault();
  },
  focusout: function () {
    $(this.hash).data('submenuTimer', setTimeout(function () {
      $(this.hash).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('submenuTimer'));  
  }
});

$('.submenu').on({
  focusout: function () {
    $(this).data('submenuTimer', setTimeout(function () {
      $(this).removeClass('submenu--active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this).data('submenuTimer'));
  },
  keydown: function (e) {
    if (e.which === 27) {
      $(this).removeClass('submenu--active');
      e.preventDefault();
    }
  }
});
.menu {
  list-style: none;
  margin: 0;
  padding: 0;
}
.menu:after {
  clear: both;
  content: '';
  display: table;
}
.menu__item {
  float: left;
  position: relative;
}

.menu__link {
  background-color: lightblue;
  color: black;
  display: block;
  padding: 0.5em 1em;
  text-decoration: none;
}
.menu__link:hover,
.menu__link:focus {
  background-color: black;
  color: lightblue;
}

.submenu {
  border: 1px solid black;
  display: none;
  left: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 100%;
}
.submenu--active {
  display: block;
}

.submenu__item {
  width: 150px;
}

.submenu__link {
  background-color: lightblue;
  color: black;
  display: block;
  padding: 0.5em 1em;
  text-decoration: none;
}

.submenu__link:hover,
.submenu__link:focus {
  background-color: black;
  color: lightblue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="menu">
  <li class="menu__item">
    <a class="menu__link" href="#menu-1">Menu 1</a>
    <ul class="submenu" id="menu-1" tabindex="-1">
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
    </ul>
  </li>
  <li class="menu__item">
    <a  class="menu__link" href="#menu-2">Menu 2</a>
    <ul class="submenu" id="menu-2" tabindex="-1">
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#1">Example 1</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#2">Example 2</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#3">Example 3</a></li>
      <li class="submenu__item"><a class="submenu__link" href="http://example.com/#4">Example 4</a></li>
    </ul>
  </li>
</ul>
lorem ipsum <a href="http://example.com/">dolor</a> sit amet.


WAI-ARIA角色和其他无障碍支持

本答案希望涵盖此功能的可访问键盘和鼠标支持的基础知识,但由于已经相当庞大,我将避免讨论WAI-ARIA角色和属性,然而我强烈建议实施者参考规范以获取应使用的角色和任何其他适当属性的详细信息。


47
这是最全面的答案,考虑到解释和易懂性。我认为这应该是被接受的答案,因为大多数其他答案只处理点击,并且只是没有任何解释的代码片段。 - Cyrille
你实际上不想绑定点击处理程序。你可以绑定点击处理程序,也可以处理用户没有鼠标的情况。这不会影响可访问性,只会为使用鼠标的用户添加功能。为一组用户添加功能并不会伤害不能使用该功能的用户。您可以提供多种关闭对话框的方式。这实际上是一个非常常见的逻辑谬误。即使其他人没有受益,给一组用户提供功能也完全没问题。我同意所有用户都应该能够获得良好的体验。 - ICW
1
通过使用blurfocusout处理程序,您仍将完全支持鼠标和触摸用户,并且它还具有支持键盘用户的附加好处。在任何时候我都没有建议您不支持鼠标用户。 - zzzzBov
1
太棒了的回答!!非常感谢。 - DarkteK
1
能够在原生JS中看到这个东西会非常有帮助。我知道这个问题是关于JQuery的,但是已经过去了14年,JQuery的答案在SO上占主导地位,所以很难学习最新的解决方案。 - Merchako

171

其他解决方案对我无效,所以我不得不使用:

if(!$(event.target).is('#foo'))
{
    // hide menu
}

编辑:纯JavaScript版本(2021-03-31)

我使用了这种方法来处理在单击菜单外部关闭下拉菜单的情况。

首先,我为组件的所有元素创建了一个自定义类名。这个类名将添加到组成菜单小部件的所有元素中。

const className = `dropdown-${Date.now()}-${Math.random() * 100}`;
我创建了一个函数来检查点击事件和被点击元素的类名。如果被点击的元素不包含我上面生成的自定义类名,它应该将show标志设置为false,并关闭菜单。
const onClickOutside = (e) => {
  if (!e.target.className.includes(className)) {
    show = false;
  }
};

然后我将点击事件处理程序附加到了窗口对象上。

// add when widget loads
window.addEventListener("click", onClickOutside);

... 最后做一些清理工作

// remove listener when destroying the widget
window.removeEventListener("click", onClickOutside);

47
这对我很有效,除此之外,我在“IF”语句中添加了 && !$(event.target).parents("#foo").is("#foo"),这样任何子元素在被单击时都不会关闭菜单。 - honyovk

142

2020年了,你可以使用event.composedPath()

来源:Event.composedPath()

composedPath()方法返回事件的路径,即将调用侦听器的对象数组。

const target = document.querySelector('#myTarget')

document.addEventListener('click', (event) => {
  const withinBoundaries = event.composedPath().includes(target)

  if (withinBoundaries) {
    target.innerText = 'Click happened inside element'
  } else {
    target.innerText = 'Click happened **OUTSIDE** element'
  }
})
/* Just to make it good looking. You don't need this */
#myTarget {
  margin: 50px auto;
  width: 500px;
  height: 500px;
  background: gray;
  border: 10px solid black;
}
<div id="myTarget">
  Click me (or not!)
</div>


这个答案虽然不能涵盖所有情况,但在2023年仍然是迄今为止最好的答案。谢谢!(是的,它没有涵盖所有场景,但它简单直接且现代化。) - André Mendonça
最佳解决方案,比if (el === event.target || el.contains(event.target))更准确... - Saif Obeidat

135

我有一个类似于 Eran's 示例的应用程序,不同之处在于当我打开菜单时,我将 click 事件附加到了 body... 就像这样:

$('#menucontainer').click(function(event) {
  $('html').one('click',function() {
    // Hide the menus
  });

  event.stopPropagation();
});

关于jQuery的one()函数的更多信息。


10
但是如果您点击菜单本身,然后在外面点击,它将不起作用 :) - vsync

60
经过研究,我找到了三种可行的解决方案。

第一种解决方案

<script>
    //The good thing about this solution is it doesn't stop event propagation.

    var clickFlag = 0;
    $('body').on('click', function () {
        if(clickFlag == 0) {
            console.log('hide element here');
            /* Hide element here */
        }
        else {
            clickFlag=0;
        }
    });
    $('body').on('click','#testDiv', function (event) {
        clickFlag = 1;
        console.log('showed the element');
        /* Show the element */
    });
</script>

第二种解决方案

<script>
    $('body').on('click', function(e) {
        if($(e.target).closest('#testDiv').length == 0) {
           /* Hide dropdown here */
        }
    });
</script>

第三种解决方案

<script>
    var specifiedElement = document.getElementById('testDiv');
    document.addEventListener('click', function(event) {
        var isClickInside = specifiedElement.contains(event.target);
        if (isClickInside) {
          console.log('You clicked inside')
        }
        else {
          console.log('You clicked outside')
        }
    });
</script>

13
第三个解决方案是最优雅的检查方式,也不需要涉及任何jQuery的额外开销。非常好。这对我帮助很大。谢谢。 - dbarth
关于“我忘记了参考页面链接”的问题:我之前从来没有听过抄袭者使用这种借口。 - Peter Mortensen
@PeterMortensen 我不明白。这里有什么可以抄袭的?我没有在这里发布博客文章或相同的代码可以抄袭... 无论如何,为了您的愉悦,我已经删除了那行 :) - Rameez Rami

43
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

这对我来说完全没问题。


1
如果尝试将此代码与自定义构建的选择器和选项菜单一起使用,模糊事件会在单击之前触发,因此不会选择任何内容。 - OzzyTheGiant

39
现在有一个插件可以实现这个功能:outside events博客文章)。
当将一个 clickoutside 处理程序绑定到一个元素时,会发生以下操作:
  • 该元素被添加到一个数组中,该数组保存所有具有 clickoutside 处理程序的元素
  • 绑定了一个 (命名空间) 为 click 的处理程序到文档 (如果尚未存在)
  • 对于文档中的任何 click,都会触发 clickoutside 事件,用于那些不等于或不是 click 事件目标的元素数组
  • 此外,clickoutside 事件的 event.target 被设置为用户单击的元素 (所以您甚至知道用户单击了什么,而不仅仅是他单击了外部)
因此,没有阻止传播的事件,并且可以在具有 outside 处理程序的元素“上方”使用其他 click 处理程序。

33

这对我完美地起作用了!

$('html').click(function (e) {
    if (e.target.id == 'YOUR-DIV-ID') {
        //do something
    } else {
        //do something
    }
});

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