clickOutside.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  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 = (binding.instance as ComponentPublicInstance<{
  32. popperRef: Nullable<HTMLElement>;
  33. }>).popperRef;
  34. const mouseUpTarget = mouseup.target as Node;
  35. const mouseDownTarget = mousedown.target as Node;
  36. const isBound = !binding || !binding.instance;
  37. const isTargetExists = !mouseUpTarget || !mouseDownTarget;
  38. const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
  39. const isSelf = el === mouseUpTarget;
  40. const isTargetExcluded =
  41. (excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
  42. (excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
  43. const isContainedByPopper =
  44. popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
  45. if (
  46. isBound ||
  47. isTargetExists ||
  48. isContainedByEl ||
  49. isSelf ||
  50. isTargetExcluded ||
  51. isContainedByPopper
  52. ) {
  53. return;
  54. }
  55. binding.value();
  56. };
  57. }
  58. const ClickOutside: ObjectDirective = {
  59. beforeMount(el, binding) {
  60. nodeList.set(el, {
  61. documentHandler: createDocumentHandler(el, binding),
  62. bindingFn: binding.value,
  63. });
  64. },
  65. updated(el, binding) {
  66. nodeList.set(el, {
  67. documentHandler: createDocumentHandler(el, binding),
  68. bindingFn: binding.value,
  69. });
  70. },
  71. unmounted(el) {
  72. nodeList.delete(el);
  73. },
  74. };
  75. export default ClickOutside;