如何检测元素外部的点击事件
技术背景
在前端开发中,常常会遇到需要检测用户是否点击了某个元素外部的情况,例如当用户点击下拉菜单外部时关闭菜单,点击模态框外部时关闭模态框等。然而,实现这一功能并不像想象中那么简单,因为需要考虑事件传播、不同设备和交互方式(如鼠标点击、键盘操作)以及可访问性等问题。
实现步骤
使用 jQuery 的 closest
方法
- 监听
document
的 click
事件。 - 检查点击目标是否不是指定元素的祖先或目标本身。
- 如果不是,则执行相应操作(如隐藏元素)。
1 2 3 4 5 6 7
| $(document).click(function(event) { var $target = $(event.target); if(!$target.closest('#menucontainer').length && $('#menucontainer').is(":visible")) { $('#menucontainer').hide(); } });
|
纯 JavaScript 实现
- 监听
document
的 click
事件。 - 使用
element.contains
方法检查点击目标是否在指定元素内部。 - 如果不在,则执行相应操作(如隐藏元素)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function hideOnClickOutside(element) { const outsideClickListener = event => { if (!element.contains(event.target) && 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 );
|
使用 focusout
事件
- 监听元素的
focusout
事件。 - 确保元素可获得焦点(添加
tabindex="-1"
)。 - 处理焦点离开元素的情况,可能需要处理一些边界情况,如焦点在元素内部转移时不关闭元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| $(document).ready(function() { $('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')); } }); });
|
处理 Esc
键关闭元素
- 监听元素的
keydown
事件。 - 检查按下的键是否为
Esc
键(键码为 27)。 - 如果是,则执行相应操作(如隐藏元素)。
1 2 3 4 5 6 7 8 9 10
| $(document).ready(function() { $('div').on({ keydown: function (e) { if (e.which === 27) { $(this).removeClass('active'); e.preventDefault(); } } }); });
|
最佳实践
- 避免使用
stopPropagation
:stopPropagation
会破坏 DOM 中的正常事件流,应尽量避免使用。 - 考虑可访问性:不仅仅依赖
click
事件,还应处理键盘操作,如 focusout
事件和 Esc
键,以确保所有用户都能正常使用。 - 清理事件监听器:当元素不再需要监听事件时,及时移除事件监听器,避免内存泄漏。
常见问题
点击元素内部元素时元素关闭
这是因为焦点在元素内部转移时触发了 focusout
事件。可以通过队列状态更改并在后续的 focusin
事件中取消来解决。
1 2 3 4 5 6 7 8 9 10 11 12
| $(document).ready(function() { $('div').on({ focusout: function () { $(this).data('timer', setTimeout(function () { $(this).removeClass('active'); }.bind(this), 0)); }, focusin: function () { clearTimeout($(this).data('timer')); } }); });
|
多次点击链接时元素状态异常
需要管理焦点状态,处理对话框触发器上的焦点事件。
1 2 3 4 5 6 7 8 9 10 11 12
| $(document).ready(function() { $('a').on({ focusout: function () { $(this.hash).data('timer', setTimeout(function () { $(this.hash).removeClass('active'); }.bind(this), 0)); }, focusin: function () { clearTimeout($(this.hash).data('timer')); } }); });
|
当 relatedTarget
为 null
时,可能会导致逻辑错误。可以通过设置元素的 tabIndex=0
使其可聚焦来解决。
1 2 3 4 5 6 7 8 9 10 11 12
| dialog = document.getElementById("dialogElement"); dialog.tabIndex = 0;
dialog.addEventListener("focusout", function (event) { if ( dialog.contains(event.relatedTarget) || !document.hasFocus() ) { return; } dialog.close(); });
|