admin-vben/packages/effects/hooks/src/use-hover-toggle.ts

164 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type { Arrayable, MaybeElementRef } from '@vueuse/core';
import type { Ref } from 'vue';
import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';
import { isFunction } from '@vben/utils';
import { useElementHover } from '@vueuse/core';
interface HoverDelayOptions {
/** 鼠标进入延迟时间 */
enterDelay?: (() => number) | number;
/** 鼠标离开延迟时间 */
leaveDelay?: (() => number) | number;
}
const DEFAULT_LEAVE_DELAY = 500; // 鼠标离开延迟时间,默认为 500ms
const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0立即响应
/**
* 监测鼠标是否在元素内部,如果在元素内部则返回 true否则返回 false
* @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
* @returns 返回一个数组,第一个元素是一个 ref表示鼠标是否在元素内部第二个元素是一个控制器可以通过 enable 和 disable 方法来控制监听器的启用和禁用
*/
export function useHoverToggle(
refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
) {
// 兼容旧版本API
const normalizedOptions: HoverDelayOptions =
typeof delay === 'number' || isFunction(delay)
? { enterDelay: DEFAULT_ENTER_DELAY, leaveDelay: delay }
: {
enterDelay: DEFAULT_ENTER_DELAY,
leaveDelay: DEFAULT_LEAVE_DELAY,
...delay,
};
const value = ref(false);
const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);
// 使用计算属性包装 refElement使其响应式变化
const refs = computed(() => {
const raw = unref(refElement);
if (raw === null) return [];
return Array.isArray(raw) ? raw : [raw];
});
// 存储所有 hover 状态
const isHovers = ref<Array<Ref<boolean>>>([]);
// 更新 hover 监听的函数
function updateHovers() {
// 停止并清理之前的作用域
hoverScopes.value.forEach((scope) => scope.stop());
hoverScopes.value = [];
isHovers.value = refs.value.map((refEle) => {
if (!refEle) {
return ref(false);
}
const eleRef = computed(() => {
const ele = unref(refEle);
return ele instanceof Element ? ele : (ele?.$el as Element);
});
// 为每个元素创建独立的作用域
const scope = effectScope();
const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);
hoverScopes.value.push(scope);
return hoverRef;
});
}
// 监听元素数量变化,避免过度执行
const elementsCount = computed(() => {
const raw = unref(refElement);
if (raw === null) return 0;
return Array.isArray(raw) ? raw.length : 1;
});
// 初始设置
updateHovers();
// 只在元素数量变化时重新设置监听器
const stopWatcher = watch(elementsCount, updateHovers, { deep: false });
const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));
function clearTimers() {
if (enterTimer.value) {
clearTimeout(enterTimer.value);
enterTimer.value = undefined;
}
if (leaveTimer.value) {
clearTimeout(leaveTimer.value);
leaveTimer.value = undefined;
}
}
function setValueDelay(val: boolean) {
clearTimers();
if (val) {
// 鼠标进入
const enterDelay = normalizedOptions.enterDelay ?? DEFAULT_ENTER_DELAY;
const delayTime = isFunction(enterDelay) ? enterDelay() : enterDelay;
if (delayTime <= 0) {
value.value = true;
} else {
enterTimer.value = setTimeout(() => {
value.value = true;
enterTimer.value = undefined;
}, delayTime);
}
} else {
// 鼠标离开
const leaveDelay = normalizedOptions.leaveDelay ?? DEFAULT_LEAVE_DELAY;
const delayTime = isFunction(leaveDelay) ? leaveDelay() : leaveDelay;
if (delayTime <= 0) {
value.value = false;
} else {
leaveTimer.value = setTimeout(() => {
value.value = false;
leaveTimer.value = undefined;
}, delayTime);
}
}
}
const hoverWatcher = watch(
isOutsideAll,
(val) => {
setValueDelay(!val);
},
{ immediate: true },
);
const controller = {
enable() {
hoverWatcher.resume();
},
disable() {
hoverWatcher.pause();
},
};
onUnmounted(() => {
clearTimers();
// 停止监听器
stopWatcher();
// 停止所有剩余的作用域
hoverScopes.value.forEach((scope) => scope.stop());
});
return [value, controller] as [typeof value, typeof controller];
}