UNPKG

vue-use-popperjs

Version:

Vue 3 popper solution powered by @popperjs

274 lines (239 loc) 6.96 kB
import { onMounted, onUnmounted, onUpdated, ref, unref, watch } from "vue"; import type { Ref } from "vue"; import { createPopper } from "@popperjs/core"; import { warn } from "./utils"; import { process } from "./node"; export type MaybeRef<T> = T | Ref<T>; export type Trigger = | "hover" | "focus" | "click-to-open" | "click-to-toggle" | "manual"; export type EventOptions = { onShow: Function; onHide: Function; }; function on( element: Element, event: string, handler: EventListenerOrEventListenerObject ) { if (element && event && handler) { element.addEventListener(event, handler, false); } } function off( element: Element, event: string, handler: EventListenerOrEventListenerObject ) { if (element && event) { element.removeEventListener(event, handler, false); } } const defaultTrigger: Trigger = "hover"; export function usePopperjs( reference: MaybeRef<Parameters<typeof createPopper>["0"]>, popper: MaybeRef<Parameters<typeof createPopper>["1"]>, options?: Partial< Parameters<typeof createPopper>["2"] & EventOptions & { trigger: MaybeRef<Trigger | undefined>; delayOnMouseover: MaybeRef<number | undefined>; delayOnMouseout: MaybeRef<number | undefined>; forceShow: MaybeRef<boolean | undefined>; } > ) { const isMounted = ref(false); onMounted(() => { isMounted.value = true; }); onUnmounted(() => { isMounted.value = false; destroy(); }); const updatedFlag = ref(true); onUpdated(() => { updatedFlag.value = !updatedFlag.value; }); const referenceRef = ref<Element>(); const popperRef = ref<HTMLElement>(); watch( () => [isMounted.value, updatedFlag.value], () => { if (!isMounted.value) return; if ((unref(reference) as any)?.$el) { referenceRef.value = (unref(reference) as any).$el; } else { referenceRef.value = unref(reference) as Element; } if ((unref(popper) as any)?.$el) { popperRef.value = (unref(popper) as any).$el; } else { popperRef.value = unref(popper); } } ); const instance = ref<ReturnType<typeof createPopper>>(); watch( () => [referenceRef.value, popperRef.value], () => { destroy(); if (!referenceRef.value) return; if (!popperRef.value) return; concrete(); } ); const concrete = () => { instance.value = createPopper(referenceRef.value!, popperRef.value!, { placement: options?.placement ?? "bottom", modifiers: options?.modifiers ?? [], strategy: options?.strategy ?? "absolute", onFirstUpdate: options?.onFirstUpdate ?? undefined, }); }; const destroy = () => { instance.value?.destroy(); instance.value = undefined; }; const visible = ref(false); const doToggle = () => (visible.value = !visible.value); const doOpen = () => (visible.value = true); const doClose = () => (visible.value = false); watch( () => [instance.value, unref(options?.trigger), unref(options?.forceShow)], () => { if (!instance.value) return; if (unref(options?.forceShow)) { visible.value = true; doOff(); return; } doOn(); } ); watch( () => unref(options?.forceShow), () => { if (unref(options?.forceShow)) return; if (unref(options?.trigger) === "manual") return; visible.value = false; } ); const timer = ref<any>(); const doMouseover = () => { if (unref(options?.delayOnMouseover) === 0) { doOpen(); } else { clearTimeout(timer.value); timer.value = setTimeout(() => { doOpen(); }, unref(options?.delayOnMouseover) ?? 200); } }; const doMouseout = () => { if (unref(options?.delayOnMouseout) === 0) { doClose(); } else { clearTimeout(timer.value); timer.value = setTimeout(() => { doClose(); }, unref(options?.delayOnMouseout) ?? 200); } }; const doOn = () => { doOff(); switch (unref(options?.trigger) ?? defaultTrigger) { case "click-to-open": { on(referenceRef.value!, "click", doOpen); on(document as any, "click", doCloseForDocument); break; } case "click-to-toggle": { on(referenceRef.value!, "click", doToggle); on(document as any, "click", doCloseForDocument); break; } case "hover": { on(referenceRef.value!, "mouseover", doMouseover); on(popperRef.value!, "mouseover", doMouseover); on(referenceRef.value!, "mouseout", doMouseout); on(popperRef.value!, "mouseout", doMouseout); break; } case "focus": { on(referenceRef.value!, "focus", doOpen); on(popperRef.value!, "focus", doOpen); on(referenceRef.value!, "blur", doClose); on(popperRef.value!, "blur", doClose); break; } case "manual": { break; } default: { throw TypeError(); } } }; const doOff = () => { off(referenceRef.value!, "click", doOpen); off(document as any, "click", doCloseForDocument); off(referenceRef.value!, "click", doToggle); off(referenceRef.value!, "mouseover", doMouseover); off(popperRef.value!, "mouseover", doMouseover); off(referenceRef.value!, "mouseout", doMouseout); off(popperRef.value!, "mouseout", doMouseout); off(referenceRef.value!, "focus", doOpen); off(popperRef.value!, "focus", doOpen); off(referenceRef.value!, "blur", doClose); off(popperRef.value!, "blur", doClose); }; const doCloseForDocument = (e: Event) => { if (referenceRef.value?.contains(e.target as Element)) return; if (popperRef.value?.contains(e.target as Element)) return; doClose(); }; watch( () => [instance.value, visible.value], () => { if (!instance.value) return; if (visible.value || unref(options?.forceShow)) { popperRef.value?.classList.remove("vue-use-popperjs-none"); options?.onShow?.(); instance.value?.update(); } else { popperRef.value?.classList.add("vue-use-popperjs-none"); options?.onHide?.(); } } ); if ( process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" ) { watch( () => [ unref(options?.trigger), unref(options?.delayOnMouseover), unref(options?.delayOnMouseout), ], () => { if ((unref(options?.trigger) ?? defaultTrigger) === "hover") return; if (unref(options?.delayOnMouseover) !== undefined) { warn("`delayOnMouseover` only works with `trigger='hover'`"); } if (unref(options?.delayOnMouseout) !== undefined) { warn("`delayOnMouseout` only works with `trigger='hover'`"); } }, { immediate: true, deep: true } ); } return { instance, visible, }; }