clickOutside.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue'
  2. import { on } from '@/utils/domUtils'
  3. import { isServer } from '@/utils/is'
  4. type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
  5. type FlushList = Map<
  6. HTMLElement,
  7. {
  8. documentHandler: DocumentHandler
  9. bindingFn: (...args: unknown[]) => unknown
  10. }
  11. >
  12. const nodeList: FlushList = new Map()
  13. let startClick: MouseEvent
  14. if (!isServer) {
  15. on(document, 'mousedown', (e: Event) => (startClick = e as MouseEvent))
  16. on(document, 'mouseup', (e: Event) => {
  17. for (const { documentHandler } of nodeList.values())
  18. documentHandler(e as MouseEvent, startClick)
  19. })
  20. }
  21. function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
  22. let excludes: HTMLElement[] = []
  23. if (Array.isArray(binding.arg)) {
  24. excludes = binding.arg
  25. }
  26. else {
  27. // due to current implementation on binding type is wrong the type casting is necessary here
  28. excludes.push(binding.arg as unknown as HTMLElement)
  29. }
  30. return function (mouseup, mousedown) {
  31. const popperRef = (
  32. binding.instance as ComponentPublicInstance<{
  33. popperRef: Nullable<HTMLElement>
  34. }>
  35. ).popperRef
  36. const mouseUpTarget = mouseup.target as Node
  37. const mouseDownTarget = mousedown.target as Node
  38. const isBound = !binding || !binding.instance
  39. const isTargetExists = !mouseUpTarget || !mouseDownTarget
  40. const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
  41. const isSelf = el === mouseUpTarget
  42. const isTargetExcluded
  43. = (excludes.length && excludes.some(item => item?.contains(mouseUpTarget)))
  44. || (excludes.length && excludes.includes(mouseDownTarget as HTMLElement))
  45. const isContainedByPopper = popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
  46. if (isBound || isTargetExists || isContainedByEl || isSelf || isTargetExcluded || isContainedByPopper)
  47. return
  48. binding.value()
  49. }
  50. }
  51. const ClickOutside: ObjectDirective = {
  52. beforeMount(el, binding) {
  53. nodeList.set(el, {
  54. documentHandler: createDocumentHandler(el, binding),
  55. bindingFn: binding.value,
  56. })
  57. },
  58. updated(el, binding) {
  59. nodeList.set(el, {
  60. documentHandler: createDocumentHandler(el, binding),
  61. bindingFn: binding.value,
  62. })
  63. },
  64. unmounted(el) {
  65. nodeList.delete(el)
  66. },
  67. }
  68. export default ClickOutside