Files
OpenIsle/frontend_nuxt/directives/clickOutside.js
2025-08-12 09:25:41 +08:00

162 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file clickOutsideDirective.js
* @description 一个用于检测元素外部点击的Vue 3自定义指令。
*
* @example
* // 在 main.js 中全局注册
* import { createApp } from 'vue'
* import App from './App.vue'
* import ClickOutside from './clickOutsideDirective.js'
*
* const app = createApp(App)
* app.directive('click-outside', ClickOutside)
* app.mount('#app')
*
* // 在组件中使用
* <div v-click-outside="myMethod">...</div>
*
* // 排除特定元素
* <div v-click-outside:[myExcludedElement]="myMethod">...</div>
* <div v-click-outside:[[el1, el2]]="myMethod">...</div>
*/
// 使用一个Map来存储所有指令绑定的元素及其对应的处理器
// 键是HTMLElement值是一个包含处理器和回调函数的对象数组
const nodeList = new Map()
// 检查是否在客户端环境以避免在SSR服务器端渲染时执行
const isClient = typeof window !== 'undefined'
// 在客户端环境中,只设置一次全局的 mousedown / mouseup 和 touchstart / touchend 监听器
if (isClient) {
let startClick
const handleStart = (e) => (startClick = e)
const handleEnd = (e) => {
// 遍历所有注册的元素和它们的处理器
for (const handlers of nodeList.values()) {
for (const { documentHandler } of handlers) {
// 调用每个处理器,传入结束和开始事件
documentHandler(e, startClick)
}
}
// 完成后重置 startClick
startClick = undefined
}
document.addEventListener('mousedown', handleStart)
document.addEventListener('touchstart', handleStart)
document.addEventListener('mouseup', handleEnd)
document.addEventListener('touchend', handleEnd)
}
/**
* 创建一个文档事件处理器。
* @param {HTMLElement} el - 指令绑定的元素。
* @param {import('vue').DirectiveBinding} binding - 指令的绑定对象。
* @returns {Function} 返回一个处理函数。
*/
function createDocumentHandler(el, binding) {
let excludes = []
// binding.arg 可以是一个元素或一个元素数组,用于排除不需要触发回调的点击
if (Array.isArray(binding.arg)) {
excludes = binding.arg
} else if (binding.arg instanceof HTMLElement) {
excludes.push(binding.arg)
}
return function (mouseup, mousedown) {
// 从组件实例中获取 popper 引用(如果存在),这对于处理下拉菜单、弹窗等很有用
const popperRef = binding.instance?.popperRef
const mouseUpTarget = mouseup.target
const mouseDownTarget = mousedown?.target
// 检查各种条件,如果满足任一条件,则不执行回调
const isBound = !binding || !binding.instance
const isTargetExists = !mouseUpTarget || !mouseDownTarget
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
const isSelf = el === mouseUpTarget
// 检查点击是否发生在任何被排除的元素内部
const isTargetExcluded =
(excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
(excludes.length && excludes.includes(mouseDownTarget))
// 检查点击是否发生在关联的 popper 元素内部
const isContainedByPopper =
popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
if (
isBound ||
isTargetExists ||
isContainedByEl ||
isSelf ||
isTargetExcluded ||
isContainedByPopper
) {
return
}
// 如果所有检查都通过,说明点击发生在外部,执行指令传入的回调函数
binding.value(mouseup, mousedown)
}
}
const ClickOutside = {
/**
* 在绑定元素的 attribute 或事件监听器被应用之前调用。
* @param {HTMLElement} el
* @param {import('vue').DirectiveBinding} binding
*/
beforeMount(el, binding) {
if (!nodeList.has(el)) {
nodeList.set(el, [])
}
nodeList.get(el).push({
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
})
},
/**
* 在包含组件的 VNode 及其子组件的 VNode 更新后调用。
* @param {HTMLElement} el
* @param {import('vue').DirectiveBinding} binding
*/
updated(el, binding) {
if (!nodeList.has(el)) {
nodeList.set(el, [])
}
const handlers = nodeList.get(el)
// 查找旧的回调函数对应的处理器
const oldHandlerIndex = handlers.findIndex((item) => item.bindingFn === binding.oldValue)
const newHandler = {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
}
if (oldHandlerIndex >= 0) {
// 如果找到了,就替换成新的处理器
handlers.splice(oldHandlerIndex, 1, newHandler)
} else {
// 否则,直接添加新的处理器
handlers.push(newHandler)
}
},
/**
* 在绑定元素的父组件卸载后调用。
* @param {HTMLElement} el
*/
unmounted(el) {
// 当元素卸载时从Map中移除它以进行垃圾回收并防止内存泄漏
nodeList.delete(el)
},
}
export default ClickOutside