use-hover-toggle.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import type { Arrayable, MaybeElementRef } from '@vueuse/core';
  2. import type { Ref } from 'vue';
  3. import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';
  4. import { isFunction } from '@vben/utils';
  5. import { useElementHover } from '@vueuse/core';
  6. interface HoverDelayOptions {
  7. /** 鼠标进入延迟时间 */
  8. enterDelay?: (() => number) | number;
  9. /** 鼠标离开延迟时间 */
  10. leaveDelay?: (() => number) | number;
  11. }
  12. const DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间,默认为 500ms
  13. const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0(立即响应)
  14. /**
  15. * 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
  16. * @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true
  17. * @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
  18. * @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
  19. */
  20. export function useHoverToggle(
  21. refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
  22. delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
  23. ) {
  24. // 兼容旧版本API
  25. const normalizedOptions: HoverDelayOptions =
  26. typeof delay === 'number' || isFunction(delay)
  27. ? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay }
  28. : {
  29. enterDelay: DEFAULT_ENTER_DELAY,
  30. leaveDelay: DEFAULT_LEAVE_DELAY,
  31. ...delay,
  32. };
  33. const value = ref(false);
  34. const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
  35. const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
  36. const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);
  37. // 使用计算属性包装 refElement,使其响应式变化
  38. const refs = computed(() => {
  39. const raw = unref(refElement);
  40. if (raw === null) return [];
  41. return Array.isArray(raw) ? raw : [raw];
  42. });
  43. // 存储所有 hover 状态
  44. const isHovers = ref<Array<Ref<boolean>>>([]);
  45. // 更新 hover 监听的函数
  46. function updateHovers() {
  47. // 停止并清理之前的作用域
  48. hoverScopes.value.forEach((scope) => scope.stop());
  49. hoverScopes.value = [];
  50. isHovers.value = refs.value.map((refEle) => {
  51. if (!refEle) {
  52. return ref(false);
  53. }
  54. const eleRef = computed(() => {
  55. const ele = unref(refEle);
  56. return ele instanceof Element ? ele : (ele?.$el as Element);
  57. });
  58. // 为每个元素创建独立的作用域
  59. const scope = effectScope();
  60. const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);
  61. hoverScopes.value.push(scope);
  62. return hoverRef;
  63. });
  64. }
  65. // 监听元素数量变化,避免过度执行
  66. const elementsCount = computed(() => {
  67. const raw = unref(refElement);
  68. if (raw === null) return 0;
  69. return Array.isArray(raw) ? raw.length : 1;
  70. });
  71. // 初始设置
  72. updateHovers();
  73. // 只在元素数量变化时重新设置监听器
  74. const stopWatcher = watch(elementsCount, updateHovers, { deep: false });
  75. const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));
  76. function clearTimers() {
  77. if (enterTimer.value) {
  78. clearTimeout(enterTimer.value);
  79. enterTimer.value = undefined;
  80. }
  81. if (leaveTimer.value) {
  82. clearTimeout(leaveTimer.value);
  83. leaveTimer.value = undefined;
  84. }
  85. }
  86. function setValueDelay(val: boolean) {
  87. clearTimers();
  88. if (val) {
  89. // 鼠标进入
  90. const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY;
  91. const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay;
  92. if (delayTime <= 0) {
  93. value.value = true;
  94. } else {
  95. enterTimer.value = setTimeout(() => {
  96. value.value = true;
  97. enterTimer.value = undefined;
  98. }, delayTime);
  99. }
  100. } else {
  101. // 鼠标离开
  102. const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY;
  103. const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay;
  104. if (delayTime <= 0) {
  105. value.value = false;
  106. } else {
  107. leaveTimer.value = setTimeout(() => {
  108. value.value = false;
  109. leaveTimer.value = undefined;
  110. }, delayTime);
  111. }
  112. }
  113. }
  114. const hoverWatcher = watch(
  115. isOutsideAll,
  116. (val) => {
  117. setValueDelay(!val);
  118. },
  119. { immediate: true },
  120. );
  121. const controller = {
  122. enable() {
  123. hoverWatcher.resume();
  124. },
  125. disable() {
  126. hoverWatcher.pause();
  127. },
  128. };
  129. onUnmounted(() => {
  130. clearTimers();
  131. // 停止监听器
  132. stopWatcher();
  133. // 停止所有剩余的作用域
  134. hoverScopes.value.forEach((scope) => scope.stop());
  135. });
  136. return [value, controller] as [typeof value, typeof controller];
  137. }