mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-15 18:01:24 +08:00
93e50e147b
* fix: can not binding multiple click-outside directives on a component * test: add test for multiple click-outside directives
118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { on } from '@element-plus/utils/dom'
|
|
import isServer from '@element-plus/utils/isServer'
|
|
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue'
|
|
|
|
|
|
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
|
|
|
|
type FlushList = Map<
|
|
HTMLElement,
|
|
{
|
|
documentHandler: DocumentHandler
|
|
bindingFn: (...args: unknown[]) => unknown
|
|
}[]
|
|
>;
|
|
|
|
const nodeList: FlushList = new Map()
|
|
|
|
let startClick: MouseEvent
|
|
|
|
if (!isServer) {
|
|
on(document, 'mousedown', (e: MouseEvent) => (startClick = e))
|
|
on(document, 'mouseup', (e: MouseEvent) => {
|
|
for (const handlers of nodeList.values()) {
|
|
for (const { documentHandler } of handlers) {
|
|
documentHandler(e, startClick)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function createDocumentHandler(
|
|
el: HTMLElement,
|
|
binding: DirectiveBinding,
|
|
): DocumentHandler {
|
|
let excludes: HTMLElement[] = []
|
|
if (Array.isArray(binding.arg)) {
|
|
excludes = binding.arg
|
|
} else if (binding.arg as unknown instanceof HTMLElement) {
|
|
// due to current implementation on binding type is wrong the type casting is necessary here
|
|
excludes.push(binding.arg as unknown as HTMLElement)
|
|
}
|
|
return function(mouseup, mousedown) {
|
|
const popperRef = (binding.instance as ComponentPublicInstance<{
|
|
popperRef: Nullable<HTMLElement>
|
|
}>).popperRef
|
|
const mouseUpTarget = mouseup.target as Node
|
|
const mouseDownTarget = mousedown?.target as Node
|
|
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 as HTMLElement)
|
|
)
|
|
const isContainedByPopper = (
|
|
popperRef &&
|
|
(
|
|
popperRef.contains(mouseUpTarget) ||
|
|
popperRef.contains(mouseDownTarget)
|
|
)
|
|
)
|
|
if (
|
|
isBound ||
|
|
isTargetExists ||
|
|
isContainedByEl ||
|
|
isSelf ||
|
|
isTargetExcluded ||
|
|
isContainedByPopper
|
|
) {
|
|
return
|
|
}
|
|
binding.value(mouseup, mousedown)
|
|
}
|
|
}
|
|
|
|
const ClickOutside: ObjectDirective = {
|
|
beforeMount(el, binding) {
|
|
// there could be multiple handlers on the element
|
|
if (!nodeList.has(el)) {
|
|
nodeList.set(el, [])
|
|
}
|
|
|
|
nodeList.get(el).push({
|
|
documentHandler: createDocumentHandler(el, binding),
|
|
bindingFn: binding.value,
|
|
})
|
|
},
|
|
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) {
|
|
// replace the old handler to the new handler
|
|
handlers.splice(oldHandlerIndex, 1, newHandler)
|
|
} else {
|
|
handlers.push(newHandler)
|
|
}
|
|
},
|
|
unmounted(el) {
|
|
// remove all listeners when a component unmounted
|
|
nodeList.delete(el)
|
|
},
|
|
}
|
|
|
|
export default ClickOutside
|