clickOutside.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import { on } from '@/utils/domUtils';
  2. import { isServer } from '@/utils/is';
  3. import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
  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: MouseEvent) => (startClick = e));
  16. on(document, 'mouseup', (e: MouseEvent) => {
  17. for (const { documentHandler } of nodeList.values()) {
  18. documentHandler(e, startClick);
  19. }
  20. });
  21. }
  22. function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
  23. let excludes: HTMLElement[] = [];
  24. if (Array.isArray(binding.arg)) {
  25. excludes = binding.arg;
  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 =
  46. popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
  47. if (
  48. isBound ||
  49. isTargetExists ||
  50. isContainedByEl ||
  51. isSelf ||
  52. isTargetExcluded ||
  53. isContainedByPopper
  54. ) {
  55. return;
  56. }
  57. binding.value();
  58. };
  59. }
  60. const ClickOutside: ObjectDirective = {
  61. beforeMount(el, binding) {
  62. nodeList.set(el, {
  63. documentHandler: createDocumentHandler(el, binding),
  64. bindingFn: binding.value,
  65. });
  66. },
  67. updated(el, binding) {
  68. nodeList.set(el, {
  69. documentHandler: createDocumentHandler(el, binding),
  70. bindingFn: binding.value,
  71. });
  72. },
  73. unmounted(el) {
  74. nodeList.delete(el);
  75. },
  76. };
  77. export default ClickOutside;