2021-12-12 23:28:03 +08:00
|
|
|
import { isClient } from '@vueuse/core'
|
2022-02-26 15:00:57 +08:00
|
|
|
import { isElement } from '@element-plus/utils'
|
2020-08-19 08:36:03 +08:00
|
|
|
|
2021-08-24 13:36:48 +08:00
|
|
|
import type {
|
|
|
|
ComponentPublicInstance,
|
|
|
|
DirectiveBinding,
|
|
|
|
ObjectDirective,
|
|
|
|
} from 'vue'
|
2022-02-11 11:03:15 +08:00
|
|
|
import type { Nullable } from '@element-plus/utils'
|
2020-08-19 08:36:03 +08:00
|
|
|
|
2021-09-04 19:29:28 +08:00
|
|
|
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
|
2020-08-19 08:36:03 +08:00
|
|
|
type FlushList = Map<
|
2021-09-04 19:29:28 +08:00
|
|
|
HTMLElement,
|
|
|
|
{
|
|
|
|
documentHandler: DocumentHandler
|
|
|
|
bindingFn: (...args: unknown[]) => unknown
|
|
|
|
}[]
|
|
|
|
>
|
2020-08-19 08:36:03 +08:00
|
|
|
|
|
|
|
const nodeList: FlushList = new Map()
|
|
|
|
|
2020-08-28 10:47:02 +08:00
|
|
|
let startClick: MouseEvent
|
2020-08-19 08:36:03 +08:00
|
|
|
|
2021-12-12 23:28:03 +08:00
|
|
|
if (isClient) {
|
2022-02-26 15:00:57 +08:00
|
|
|
document.addEventListener('mousedown', (e: MouseEvent) => (startClick = e))
|
|
|
|
document.addEventListener('mouseup', (e: MouseEvent) => {
|
2021-06-25 17:10:29 +08:00
|
|
|
for (const handlers of nodeList.values()) {
|
|
|
|
for (const { documentHandler } of handlers) {
|
2022-01-04 09:15:15 +08:00
|
|
|
documentHandler(e as MouseEvent, startClick)
|
2021-06-25 17:10:29 +08:00
|
|
|
}
|
2020-08-19 08:36:03 +08:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function createDocumentHandler(
|
|
|
|
el: HTMLElement,
|
2021-09-04 19:29:28 +08:00
|
|
|
binding: DirectiveBinding
|
2020-08-19 08:36:03 +08:00
|
|
|
): DocumentHandler {
|
2020-08-28 10:47:02 +08:00
|
|
|
let excludes: HTMLElement[] = []
|
|
|
|
if (Array.isArray(binding.arg)) {
|
|
|
|
excludes = binding.arg
|
2022-02-26 15:00:57 +08:00
|
|
|
} else if (isElement(binding.arg)) {
|
2020-08-28 10:47:02 +08:00
|
|
|
// due to current implementation on binding type is wrong the type casting is necessary here
|
|
|
|
excludes.push(binding.arg as unknown as HTMLElement)
|
|
|
|
}
|
2021-09-04 19:29:28 +08:00
|
|
|
return function (mouseup, mousedown) {
|
|
|
|
const popperRef = (
|
|
|
|
binding.instance as ComponentPublicInstance<{
|
|
|
|
popperRef: Nullable<HTMLElement>
|
|
|
|
}>
|
|
|
|
).popperRef
|
2020-08-28 10:47:02 +08:00
|
|
|
const mouseUpTarget = mouseup.target as Node
|
2021-04-06 13:28:57 +08:00
|
|
|
const mouseDownTarget = mousedown?.target as Node
|
2020-08-28 10:47:02 +08:00
|
|
|
const isBound = !binding || !binding.instance
|
|
|
|
const isTargetExists = !mouseUpTarget || !mouseDownTarget
|
2021-09-04 19:29:28 +08:00
|
|
|
const isContainedByEl =
|
|
|
|
el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
|
2020-08-28 10:47:02 +08:00
|
|
|
const isSelf = el === mouseUpTarget
|
|
|
|
|
|
|
|
const isTargetExcluded =
|
2021-09-04 19:29:28 +08:00
|
|
|
(excludes.length &&
|
|
|
|
excludes.some((item) => item?.contains(mouseUpTarget))) ||
|
|
|
|
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement))
|
|
|
|
const isContainedByPopper =
|
2020-08-28 10:47:02 +08:00
|
|
|
popperRef &&
|
2021-09-04 19:29:28 +08:00
|
|
|
(popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
|
2020-08-19 08:36:03 +08:00
|
|
|
if (
|
2020-08-28 10:47:02 +08:00
|
|
|
isBound ||
|
|
|
|
isTargetExists ||
|
|
|
|
isContainedByEl ||
|
|
|
|
isSelf ||
|
|
|
|
isTargetExcluded ||
|
|
|
|
isContainedByPopper
|
2020-08-19 08:36:03 +08:00
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
2021-05-24 17:29:19 +08:00
|
|
|
binding.value(mouseup, mousedown)
|
2020-08-19 08:36:03 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const ClickOutside: ObjectDirective = {
|
2021-09-09 19:41:10 +08:00
|
|
|
beforeMount(el: HTMLElement, binding: DirectiveBinding) {
|
2021-06-25 17:10:29 +08:00
|
|
|
// there could be multiple handlers on the element
|
|
|
|
if (!nodeList.has(el)) {
|
|
|
|
nodeList.set(el, [])
|
|
|
|
}
|
|
|
|
|
|
|
|
nodeList.get(el).push({
|
2020-08-19 08:36:03 +08:00
|
|
|
documentHandler: createDocumentHandler(el, binding),
|
|
|
|
bindingFn: binding.value,
|
|
|
|
})
|
|
|
|
},
|
2021-09-09 19:41:10 +08:00
|
|
|
updated(el: HTMLElement, binding: DirectiveBinding) {
|
2021-06-25 17:10:29 +08:00
|
|
|
if (!nodeList.has(el)) {
|
|
|
|
nodeList.set(el, [])
|
|
|
|
}
|
|
|
|
|
|
|
|
const handlers = nodeList.get(el)
|
2021-09-04 19:29:28 +08:00
|
|
|
const oldHandlerIndex = handlers.findIndex(
|
|
|
|
(item) => item.bindingFn === binding.oldValue
|
|
|
|
)
|
2021-06-25 17:10:29 +08:00
|
|
|
const newHandler = {
|
2020-08-19 08:36:03 +08:00
|
|
|
documentHandler: createDocumentHandler(el, binding),
|
|
|
|
bindingFn: binding.value,
|
2021-06-25 17:10:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (oldHandlerIndex >= 0) {
|
|
|
|
// replace the old handler to the new handler
|
|
|
|
handlers.splice(oldHandlerIndex, 1, newHandler)
|
|
|
|
} else {
|
|
|
|
handlers.push(newHandler)
|
|
|
|
}
|
2020-08-19 08:36:03 +08:00
|
|
|
},
|
2021-09-09 19:41:10 +08:00
|
|
|
unmounted(el: HTMLElement) {
|
2021-06-25 17:10:29 +08:00
|
|
|
// remove all listeners when a component unmounted
|
2020-08-19 08:36:03 +08:00
|
|
|
nodeList.delete(el)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
export default ClickOutside
|