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

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

jQuery 可能会发生这种情况吗?

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

答案

注意:应该避免使用stopEventPropagation() ,因为它会破坏 DOM 中的正常事件流。有关更多信息,请参见本文 。考虑改用此方法

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

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

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

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

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

$(document).click(function(event) { 
  $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) => {
    $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 的人。这是上面的纯香草代码(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()

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

这个问题之所以如此流行并且答案如此之多,是因为它看似复杂。经过将近八年的时间和数十个答案,我真的很惊讶地看到对可访问性的关注很少。

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

这是一个崇高的原因,是实际的问题。问题的标题(即大多数答案似乎试图解决的问题)包含不幸的鲱鱼。

提示:这是“点击”一词!

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

如果要绑定单击处理程序以关闭对话框,则您已经失败了。您失败的原因是,并非所有人都触发click事件。不使用鼠标的用户将可以通过按Tab来退出对话框(并且弹出菜单可以说是对话框的一种),然后他们将无法在不随后触发click情况下读取对话框后面的内容。事件。

因此,让我们改一下这个问题。

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

这是目标。不幸的是,现在我们需要用userisfinishedwiththedialog事件绑定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>


如果您在演示中玩了超过一分钟,则应该很快开始发现问题。

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

解决方法是在状态循环上排队状态更改。对于不支持setImmediate浏览器,可以使用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>


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 角色和属性 ,但是我强烈建议实现者参考规范以了解详细信息。他们应该使用什么角色以及任何其他适当的属性。

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

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

我有一个类似于 Eran 的示例的应用程序,除了在打开菜单时将 click 事件附加到主体上。

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

  event.stopPropagation();
});

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

经过研究,我找到了三种可行的解决方案(我忘记了页面链接以供参考)

第一个解决方案

<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>
$("#menuscontainer").click(function() {
    $(this).focus();
});
$("#menuscontainer").blur(function(){
    $(this).hide();
});

对我有用。

现在有一个用于此的插件: 外部事件博客文章

clickoutside处理程序(WLOG)绑定到元素时,将发生以下情况:

  • 将该元素添加到一个数组,该数组包含带有clickoutside处理程序的所有元素
  • 命名空间的单击处理程序已绑定到文档(如果尚未存在)
  • 在文档中的任何单击上, 都会为该数组中与click -events 目标不相等或为父的那些元素触发clickoutside事件
  • 此外, clickoutside事件的 event.target 设置为用户单击的元素(因此,您甚至知道用户单击了什么,而不仅仅是他在外部单击了)

因此,不会停止任何事件的传播,并且可以使用外部处理程序在元素 “上方” 使用其他单击处理程序。

这完全适合我!

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

我不认为您真正需要的是在用户单击外部时关闭菜单。您需要的是,当用户单击页面上的任意位置时,菜单关闭。如果单击菜单,或者关闭菜单,它应该关闭吗?

上面找不到满意的答案,促使我前几天写了这篇博客 。对于更加学究的人来说,有许多陷阱需要注意:

  1. 如果在单击时将 click 事件处理程序附加到 body 元素,请确保在关闭菜单并取消绑定事件之前等待第二次单击。否则,打开菜单的 click 事件将冒泡到必须关闭菜单的侦听器中。
  2. 如果在 click 事件上使用 event.stopPropogation(),则页面中的其他任何元素都不能具有 “单击任何位置关闭” 功能。
  3. 将 click 事件处理程序无限期地附加到 body 元素上并不是一种有效的解决方案
  4. 比较事件的目标及其处理程序的创建者的父级,假定您想要的是单击菜单时关闭菜单,而您真正想要的是单击页面上的任意位置时关闭菜单。
  5. 在 body 元素上侦听事件将使您的代码更加脆弱。像这样无辜的样式会破坏它: body { margin-left:auto; margin-right: auto; width:960px;}