题 如何检测元素外的点击?


我有一些HTML菜单,当用户点击这些菜单的头部时,我会完全显示。当用户点击菜单区域外时,我想隐藏这些元素。

jQuery可以这样吗?

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

2027


起源


以下是此策略的示例: jsfiddle.net/tedp/aL7Xe/1 - Ted
正如汤姆所说,你会想读 css-tricks.com/dangers-stopping-event-propagation 在使用这种方法之前。那个jsfiddle工具虽然很酷。 - Jon Coombs
得到对元素的引用,然后是event.target,最后!=或==然后它们都会相应地执行代码。 - Rohit Kumar
我推荐使用 github.com/gsantiago/jquery-clickout :) - Rômulo M. Farias
尝试使用 event.path。 http://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element/43405204#43405204 - Dan Philip


答案:


注意:使用 stopEventPropagation() 是应该避免的,因为它打破了DOM中的正常事件流。看到 本文 了解更多信息。考虑使用 这种方法 代替。

将单击事件附加到关闭窗口的文档正文。将单独的单击事件附加到容器,该容器将停止传播到文档正文。

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

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

1620



这打破了#menucontainer中包含的许多内容的标准行为,包括按钮和链接。我很惊讶这个答案如此受欢迎。 - Art
这不会破坏#menucontainer中任何内容的行为,因为它位于传播链的​​底部,对于它内部的任何东西。 - Eran Galperin
它非常美丽,但你应该使用 $('html').click() 不是身体。身体总是有其内容的高度。它没有很多内容或屏幕非常高,它只适用于身体填充的部分。 - meo
我也很惊讶这个解决方案获得了如此多的选票。对于具有stopPropagation的外部元素,这将失败 jsfiddle.net/Flandre/vaNFw/3 - Andre
Philip Walton非常清楚地解释了为什么这个答案不是最好的解决方案: css-tricks.com/dangers-stopping-event-propagation - Tom


你可以听一个 点击 活动 document 然后确保 #menucontainer 通过使用不是祖先或被点击元素的目标 .closest()

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

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

编辑 - 2017-06-23

如果您打算关闭菜单并想要停止侦听事件,也可以在事件监听器之后进行清理。此函数将仅清除新创建的侦听器,并保留其他任何单击侦听器 document。使用ES2015语法:

export function hideOnClickOutside(selector) {
  const outsideClickListener = (event) => {
    if (!$(event.target).closest(selector).length) {
      if ($(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)) { // or use: event.target.closest(selector) === null
            if (isVisible(element)) {
                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可以在这里找到: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest


1127



我尝试了很多其他的答案,但只有这个有效。谢谢。我最终使用的代码是:$(document).click(function(event){if($(event.target).closest('。window')。length == 0){$('。window' ).fadeOut('fast');}}); - Pistos
我实际上最终得到了这个解决方案,因为它更好地支持同一页面上的多个菜单,在第一个打开时点击第二个菜单将在stopPropagation解决方案中保留第一个菜单。 - umassthrower
对于页面上的多个项目,这是一个非常好的解决方案。 - jsgroove
很好的答案。当您有多个要关闭的项目时,这是要走的路。 - John
没有jQuery  - !element.contains(event.target) 运用 Node.contains() - Alex Ross


如何检测元素外的点击?

这个问题如此受欢迎并且答案如此之多的原因在于它看起来很复杂。经过近八年的时间和几十个答案,我真的很惊讶地看到对可访问性的关注度很低。

当用户点击菜单区域外时,我想隐藏这些元素。

这是一个崇高的事业,而且是 实际 问题。问题的标题 - 这是大多数答案似乎试图解决的问题 - 包含一个不幸的红鲱鱼。

提示:这就是这个词 “点击”

您实际上并不想绑定点击处理程序。

如果您绑定了点击处理程序以关闭对话框,那么您已经失败了。你失败的原因是不是每个人都会触发 click 事件。不使用鼠标的用户可以通过按下来转义对话框(并且您的弹出菜单可以说是一种对话框) 标签,然后他们将无法在不随后触发的情况下阅读对话框后面的内容 click 事件。

让我们重新解释一下这个问题。

如何在用户完成对话时关闭对话框?

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

那么我们怎样才能检测到用户已经完成了对话框的使用?

focusout 事件

一个好的开始是确定焦点是否已离开对话框。

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

jQuery的 focusout 会做得很好。如果你不能使用jQuery,那么你可以使用 blur 在捕获阶段:

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

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

$('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>


如果您使用该演示超过一分钟,您应该很快就会看到问题。

第一个是对话框中的链接不可点击。尝试单击它或选项卡到它将导致对话关闭在交互发生之前。这是因为聚焦内部元素会触发a focusout 触发前的事件 focusin 事件再次。

修复是在事件循环上对状态更改进行排队。这可以通过使用来完成 setImmediate(...), 要么 setTimeout(..., 0) 对于不支持的浏览器 setImmediate。排队后,可以通过后续取消 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>


退出 键

如果您认为自己是通过处理焦点状态来完成的,那么您可以采取更多措施来简化用户体验。

这通常是一个“很好的”功能,但是当你有一个任何类型的模态或弹出窗口时,这是常见的 退出 钥匙将关闭它。

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角色和属性, 但是,我 高度 建议实施者参考规范,了解他们应该使用哪些角色以及任何其他适当的属性。


188



这是最完整的答案,考虑到解释和可访问性。我认为这应该是公认的答案,因为大多数其他答案只处理点击而且只是代码片段丢弃而没有任何解释。 - Cyrille
很棒,很好解释。我只是在React组件上使用了这种方法并且非常感谢 - David Lavieri
感谢@zzzzBov的深刻回答,我试图在vanilla JS中实现它,而且我对所有jquery的东西都有点失落。香草js有什么相似之处吗? - HendrikEng
@zzzzBov不,我不是在找你写一个免费的jQuery版本,我会尝试这样做,我想最好是在这里问一个新问题,如果我真的卡住了。再次感谢。 - HendrikEng
虽然这是检测单击自定义下拉列表或其他输入的一种很好的方法,但由于两个原因,它绝不应该是模态或弹出窗口的首选方法。当用户切换到另一个选项卡或窗口时,模态将关闭,或者打开上下文菜单,这实际上很烦人。此外,“click”事件会在鼠标向上时触发,而“focusout”事件会触发您按下鼠标的瞬间。通常,如果按下鼠标按钮然后放开,按钮仅执行操作。为模态执行此操作的正确,可访问的方法是添加一个tabbable关闭按钮。 - Kevin


这里的其他解决方案对我不起作用所以我不得不使用:

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

127



我已经发布了另一个实例,说明如何使用event.target来避免在将它们嵌入到您自己的弹出框中时触发其他Jquery UI小部件外部单击html处理程序: 获得原始目标的最佳方式 - Joey T
除了我补充说,这对我有用 && !$(event.target).parents("#foo").is("#foo") 在 - 的里面 IF 声明,以便任何子元素在单击时不会关闭菜单。 - honyovk
@Frug你的回复可能很有价值,所以你有5个,但我对你的回复感到困惑。谁是MBJ。我希望根据你的回复创建更好的解决方案。 - Satya Prakash
MBJ可能是在我上面发表评论的人的旧名称。用户更改名称,这可能会使其变得困难,但请注意我上面的评论已添加 .parents("#foo") - Frug
处理深度嵌套的简洁改进就是使用 .is('#foo, #foo *')但是 我不建议绑定点击处理程序来解决此问题。 - zzzzBov


我有一个类似于Eran示例的应用程序,除了我在打开菜单时将click事件附加到正文...有点像这样:

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

  event.stopPropagation();
});

有关的更多信息 jQuery的 one() 功能


118



但是如果你单击菜单本身,然后在外面,它将无法正常工作:) - vsync
在将click侦听器绑定到body之前放入event.stopProgagantion()会很有帮助。 - Jasper Kennis
这个问题是“one”适用于多次向数组添加事件的jQuery方法。因此,如果您单击菜单多次打开它,事件将再次绑定到正文并尝试多次隐藏菜单。应该使用故障保护来解决此问题。 - marksyzm
绑定后使用 .one  - 里面的 $('html') 处理程序 - 写一个 $('html').off('click')。 - Cody
@Cody我认为没有帮助。该 one 处理程序将自动调用 off (正如它在jQuery文档中所示)。 - Mariano Desanze


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

对我来说就好了。


32



这是我使用的那个。它可能不是完美的,但作为一个爱好程序员,它的简单到足以清楚地理解。 - kevtrout
模糊是一种外在的移动 #menucontainer 问题是关于点击 - borrel
@borrel模糊了 不 移动到容器外面。模糊是焦点的反面,你正在想鼠标。当我创建“单击以编辑”文本时,此解决方案对我来说特别有效,我在单击时在纯文本和输入字段之间来回切换。 - parker.sikand
我不得不补充一下 tabindex="-1" 到了 #menuscontainer 使它工作。似乎如果你在容器中放入一个输入标签并点击它,容器就会被隐藏起来。 - tyrion
mouseleave事件更适合菜单和容器(参考: w3schools.com/jquery/...) - Evgenia Manolova


现在有一个插件: 外部事件 (博客文章

当a时发生以下情况 clickoutside handler(WLOG)绑定到一个元素:

  • 该元素被添加到一个包含所有元素的数组中 clickoutside 处理器
  • 一个 (命名空间中点击 处理程序绑定到文档(如果尚未存在)
  • 任何 点击 在文件中, clickoutside 触发该数组中那些不等于或者父元素的元素 点击 - 事件目标
  • 另外,event.target为 clickoutside event被设置为用户点击的元素(所以你甚至可以知道用户点击了什么,而不仅仅是他点击了外面)

因此没有任何事件被阻止传播和其他事件 点击 处理程序可以在具有外部处理程序的元素“之上”使用。


31



不错的插件。关于“外部事件”的链接已经死亡,而博客帖子链接是活着的,并为“clickoutside”类活动提供了一个非常有用的插件。这也是麻省理工学院的许可。 - TechNyquist
伟大的插件。工作得很好。用法如下: $( '#element' ).on( 'clickoutside', function( e ) { .. } ); - Gavin


这完全适合我!

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

26



这个解决方案适用于我正在做的事情,我仍然需要点击事件来冒泡,谢谢。 - Mike