如何检测元素外的点击?

IT技术 javascript jquery click
2020-12-23 16:10:25

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

jQuery 可以实现这样的功能吗?

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

注意:使用stopPropagation是应该避免的,因为它会破坏 DOM 中的正常事件流。有关更多信息,请参阅此 CSS 技巧文章考虑改用这种方法

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

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

$('#menucontainer').click(function(event){
  event.stopPropagation();
});
这打破了#menucontainer 中包含的许多东西的标准行为,包括按钮和链接。我很惊讶这个答案如此受欢迎。
2021-02-12 16:10:25
我也很惊讶这个解决方案得到了这么多票。对于具有 stopPropagation jsfiddle.net/Flandre/vaNFw/3之外的任何元素,这将失败
2021-02-13 16:10:25
这不会破坏#menucontainer 中任何内容的行为,因为它位于其中任何内容的传播链的底部。
2021-02-26 16:10:25
它非常美丽,但你不应该使用$('html').click()身体。正文始终具有其内容的高度。内容不多或屏幕很高,只对身体填充的部分有效。
2021-02-26 16:10:25
菲利普沃尔顿很好地解释了为什么这个答案不是最好的解决方案:css-tricks.com/dangers-stopping-event-propagation
2021-03-05 16:10:25

您可以在 on 上侦听单击事件document,然后使用 确保#menucontainer它不是被单击元素的祖先或目标 .closest()

如果不是,则单击的元素在 之外,#menucontainer您可以安全地隐藏它。

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

编辑 – 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()
        }
    }
没有 jQuery -!element.contains(event.target)使用Node.contains()
2021-02-22 16:10:25
我实际上最终选择了这个解决方案,因为它更好地支持同一页面上的多个菜单,其中在第一个打开时单击第二个菜单将使第一个在 stopPropagation 解决方案中保持打开状态。
2021-02-26 16:10:25
优秀的答案。当您有多个要关闭的项目时,这是一种方法。
2021-03-04 16:10:25
我尝试了许多其他答案,但只有这个有效。谢谢。我最终使用的代码是这样的: $(document).click(function(event) { if( $(event.target).closest('.window').length == 0 ) { $('.window' ).fadeOut('fast'); } } );
2021-03-08 16:10:25

如何检测元素外的点击?

这个问题如此受欢迎并有如此多的答案的原因是它看似复杂。在将近八年和几十个答案之后,我真的很惊讶地看到对可访问性的关注如此之少。

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

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

提示:是“点击”这个词

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

如果您正在绑定单击处理程序以关闭对话框,那么您已经失败了。你失败的原因是不是每个人都会触发click事件。不使用鼠标的用户将能够通过按 退出您的对话框(并且您的弹出菜单可以说是一种对话框)Tab,然后他们将无法在不随后触发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>


如果您玩该演示超过一分钟,您应该很快就会发现问题。

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

解决方法是在事件循环中对状态更改进行排队。这可以通过使用来完成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({
  focusout: function () {
    $(this.hash).data('timer', setTimeout(function () {
      $(this.hash).removeClass('active');
    }.bind(this), 0));
  },
  focusin: function () {
    clearTimeout($(this.hash).data('timer'));  
  }
});


Esc 钥匙

如果您认为处理焦点状态已经完成,那么您还可以做更多的事情来简化用户体验。

这通常是一个“很高兴拥有”的功能,但是当您有任何类型的模式或弹出窗口时,Esc键会关闭它是很常见的

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


如果您知道对话框中有可聚焦元素,则无需直接聚焦对话框。如果您正在构建菜单,则可以改为关注第一个菜单项。

click: function (e) {
  $(this.hash)
    .toggleClass('submenu--active')
    .find('a:first')
    .focus();
  e.preventDefault();
}


WAI-ARIA 角色和其他辅助功能支持

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

这是最完整的答案,并考虑了解释和可访问性。我认为这应该是公认的答案,因为大多数其他答案只处理点击,只是没有任何解释的代码片段。
2021-02-15 16:10:25
You don't actually want to bind click handlers.您可以绑定点击处理程序,也可以处理用户没有鼠标的情况。它不会损害可访问性,它只会为使用鼠标的用户添加功能。向一组用户添加功能不会伤害无法使用该功能的用户。您可以提供多种关闭diablog 的方法。这实际上是一个非常常见的逻辑谬误。即使其他人没有受益,向一组用户提供一项功能也完全没有问题。我同意所有用户都应该能够获得良好的体验
2021-03-02 16:10:25
@ICW,通过使用blurfocusout处理程序,您仍将完全支持鼠标和触摸用户,并且它还具有支持键盘用户的额外好处。我从来没有建议你支持鼠标用户。
2021-03-03 16:10:25

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

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);
这对我有用,除了我&& !$(event.target).parents("#foo").is("#foo")IF语句中添加以便单击时任何子元素都不会关闭菜单。
2021-02-28 16:10:25

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

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

  event.stopPropagation();
});

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

但是如果你点击菜单本身,然后在外面,它就行不通了:)
2021-02-19 16:10:25