clickOutside.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  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 = popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
  46. if (isBound || isTargetExists || isContainedByEl || isSelf || isTargetExcluded || isContainedByPopper) {
  47. return;
  48. }
  49. binding.value();
  50. };
  51. }
  52. const ClickOutside: ObjectDirective = {
  53. beforeMount(el, binding) {
  54. nodeList.set(el, {
  55. documentHandler: createDocumentHandler(el, binding),
  56. bindingFn: binding.value,
  57. });
  58. },
  59. updated(el, binding) {
  60. nodeList.set(el, {
  61. documentHandler: createDocumentHandler(el, binding),
  62. bindingFn: binding.value,
  63. });
  64. },
  65. unmounted(el) {
  66. nodeList.delete(el);
  67. },
  68. };
  69. export default ClickOutside;