click outside component
在 vue 中以 script setup
的形式实现一个性能还不错的 <ClickOutside />
组件, 理论上适用于任何语言框架, 只需要适配一下 <template>
及 <script>
的部分即可, 同时也是可以适用于原生 JS 的.
<template>
<div ref="targetRef">
<slot />
</div>
</template>
<script setup>
import { intersection } from 'lodash'
import { ref, unref, defineEmits, defineProps, onBeforeUnmount } from 'vue'
const targetRef = ref(null)
const emits = defineEmits(['trigger'])
// 这两个属性通常不会发生变化, 都是初始就预设好的, 所以就直接解构使用了, 在开发中不推荐这么使用
// eslint-disable-next-line vue/no-setup-props-destructure
const { ignore, capture } = defineProps({
// 需要忽略的元素类名,多个类名以 , 分割
ignore: {
type: String,
default: 'el-popper'
},
// 是否以捕获的形式触发事件
capture: {
type: Boolean,
default: true
}
})
// 过滤不合法的元素选择器
const IGNORES = ignore.split(',').filter(Boolean)
const clickEventHandler = async(event) => {
const el = event.target
// 实现的关键, 这个方法返回事件触发时, 在事件冒泡的完整路径
const composed = event.composedPath() // ^[1] Event.composedPath()
if (
!el ||
el === unref(targetRef) || // 如果点击的元素是当前组件本身
composed.includes(unref(targetRef)) || // 如果点击的是组件的子元素时
// 如果有设置忽略的元素类名并且被包含在内的话
ignore && composed.some(element =>
!!intersection(IGNORES, element.classList).length)
// ↑ 这里也可以自己手写判断数据交集的实现, 主要是判断元素的类名是否处于忽略的列表中
) { return }
emits('trigger', event)
}
// 因为我并不需要操作或获取当前组件的 dom 元素
// 又因为 document 是全局对象, 不管组件是否挂载都可以访问到
// 所以在添加事件的时候可以不用放在 onMounted() 里
document.addEventListener(
'click',
clickEventHandler,
{
capture,
passive: true
}
)
onBeforeUnmount(() => {
// 组件卸载时一定记得清理副作用
document.removeEventListener(
'click',
clickEventHandler,
{
capture,
passive: true
}
)
})
</script>
<script>
// 给组件添加名字, 以及不在 DOM 元素上显式外部给组件设置的属性
export default {
name: 'TrClickOutside',
inheritAttrs: false
}
</script>