import { nextTick, inject, provide, watch, type InjectionKey, onBeforeUnmount, watchEffect, onMounted, type Ref, type CSSProperties, defineComponent, ref } from "vue"; import { addCSS, css, h as djh } from "@/util.ts"; type TooltipContext = { show: (newText: string, x: number, y: number) => void, hide: () => void, }; const tooltipContext = Symbol('tooltip') as InjectionKey; const tooltipStyles = css` .tooltip-container { display: inline-block; } .tooltip-carrier { opacity: 0; display: block; pointer-events: none; background-color: black; border: white solid 1px; color: white; padding: 10px; position: absolute; z-index: 1; overflow: hidden; height: 0; width: 0; position: absolute; transition: opacity 200ms, height 200ms, width 200ms; .text-carrier { position: absolute; width: 350px; font-size: 16px; font-family: "Roboto", serif; display: block; overflow: hidden; } }`; export function setupTooltip(options: { carrier: Ref }) { const { carrier } = options; addCSS('tooltip-carrier', tooltipStyles); watchEffect(() => { if (carrier.value) { carrier.value.classList.add('tooltip-carrier'); carrier.value.appendChild(djh('div', { className: 'text-carrier' })); } }); const listener = (pos: { x: number, y: number }) => { if (carrier.value && getComputedStyle(carrier.value).opacity !== "0") { if (pos.x + 15 + carrier.value.clientWidth <= document.body.scrollWidth) { carrier.value.style.left = (pos.x + 15) + "px"; } else { carrier.value.style.left = (document.body.scrollWidth - carrier.value.clientWidth - 5) + "px"; } if (pos.y + carrier.value.clientHeight <= document.body.scrollHeight) { carrier.value.style.top = pos.y + "px"; } else { carrier.value.style.top = (document.body.scrollHeight - carrier.value.clientHeight - 5) + "px"; } } }; const active = ref(false); watch(active, async () => { const tooltipCarrier = carrier.value; if (tooltipCarrier) { tooltipCarrier.style.opacity = active.value ? '1' : '0'; tooltipCarrier.style.width = active.value ? '350px' : '0'; await nextTick(); if (active.value) { if (tooltipCarrier.firstChild?.nodeType === Node.ELEMENT_NODE) { const computedHeight = getComputedStyle(tooltipCarrier.firstChild as Element).height; tooltipCarrier.style.height = computedHeight; } } else { tooltipCarrier.style.height = '0'; } } }); onMounted(() => document.addEventListener("mousemove", (e) => listener({ x: e.pageX, y: e.pageY }))); onBeforeUnmount(() => document.removeEventListener("mousemove", (e) => listener({ x: e.pageX, y: e.pageY }))); const ctx: TooltipContext = { show(tooltip, x, y) { if (carrier.value) { (carrier.value.firstChild as HTMLElement)!.innerHTML = tooltip; } active.value = true; listener({ x, y }); }, hide() { active.value = false; }, }; provide(tooltipContext, ctx); } export default defineComponent({ name: "dj-tooltip", props: { tooltip: { type: String, required: true, }, }, setup(props, { slots, attrs }) { const tooltip = inject(tooltipContext, () => { throw new Error('No tooltip context'); }, true); onBeforeUnmount(() => tooltip.hide()); return () => <>
tooltip.show(props.tooltip, e.pageX, e.pageY)} onMouseleave={() => tooltip.hide()}> {slots.default && }
; }, });