index.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { isClient } from '@vueuse/core'
  2. import { isElement } from '@element-plus/utils'
  3. import type {
  4. ComponentPublicInstance,
  5. DirectiveBinding,
  6. ObjectDirective,
  7. } from 'vue'
  8. type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
  9. type FlushList = Map<
  10. HTMLElement,
  11. {
  12. documentHandler: DocumentHandler
  13. bindingFn: (...args: unknown[]) => unknown
  14. }[]
  15. >
  16. const nodeList: FlushList = new Map()
  17. let startClick: MouseEvent
  18. if (isClient) {
  19. document.addEventListener('mousedown', (e: MouseEvent) => (startClick = e))
  20. document.addEventListener('mouseup', (e: MouseEvent) => {
  21. for (const handlers of nodeList.values()) {
  22. for (const { documentHandler } of handlers) {
  23. documentHandler(e as MouseEvent, startClick)
  24. }
  25. }
  26. })
  27. }
  28. function createDocumentHandler(
  29. el: HTMLElement,
  30. binding: DirectiveBinding
  31. ): DocumentHandler {
  32. let excludes: HTMLElement[] = []
  33. if (Array.isArray(binding.arg)) {
  34. excludes = binding.arg
  35. } else if (isElement(binding.arg)) {
  36. // due to current implementation on binding type is wrong the type casting is necessary here
  37. excludes.push(binding.arg as unknown as HTMLElement)
  38. }
  39. return function (mouseup, mousedown) {
  40. const popperRef = (
  41. binding.instance as ComponentPublicInstance<{
  42. popperRef: HTMLElement
  43. }>
  44. ).popperRef
  45. const mouseUpTarget = mouseup.target as Node
  46. const mouseDownTarget = mousedown?.target as Node
  47. const isBound = !binding || !binding.instance
  48. const isTargetExists = !mouseUpTarget || !mouseDownTarget
  49. const isContainedByEl =
  50. el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
  51. const isSelf = el === mouseUpTarget
  52. const isTargetExcluded =
  53. (excludes.length &&
  54. excludes.some((item) => item?.contains(mouseUpTarget))) ||
  55. (excludes.length && excludes.includes(mouseDownTarget as HTMLElement))
  56. const isContainedByPopper =
  57. popperRef &&
  58. (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
  59. if (
  60. isBound ||
  61. isTargetExists ||
  62. isContainedByEl ||
  63. isSelf ||
  64. isTargetExcluded ||
  65. isContainedByPopper
  66. ) {
  67. return
  68. }
  69. binding.value(mouseup, mousedown)
  70. }
  71. }
  72. const ClickOutside: ObjectDirective = {
  73. beforeMount(el: HTMLElement, binding: DirectiveBinding) {
  74. // there could be multiple handlers on the element
  75. if (!nodeList.has(el)) {
  76. nodeList.set(el, [])
  77. }
  78. nodeList.get(el)!.push({
  79. documentHandler: createDocumentHandler(el, binding),
  80. bindingFn: binding.value,
  81. })
  82. },
  83. updated(el: HTMLElement, binding: DirectiveBinding) {
  84. if (!nodeList.has(el)) {
  85. nodeList.set(el, [])
  86. }
  87. const handlers = nodeList.get(el)!
  88. const oldHandlerIndex = handlers.findIndex(
  89. (item) => item.bindingFn === binding.oldValue
  90. )
  91. const newHandler = {
  92. documentHandler: createDocumentHandler(el, binding),
  93. bindingFn: binding.value,
  94. }
  95. if (oldHandlerIndex >= 0) {
  96. // replace the old handler to the new handler
  97. handlers.splice(oldHandlerIndex, 1, newHandler)
  98. } else {
  99. handlers.push(newHandler)
  100. }
  101. },
  102. unmounted(el: HTMLElement) {
  103. // remove all listeners when a component unmounted
  104. nodeList.delete(el)
  105. },
  106. }
  107. export default ClickOutside