Files
djledda-web/app/DjTooltip.tsx
2025-12-20 21:16:00 +01:00

131 lines
4.1 KiB
TypeScript

import { nextTick, inject, provide, watch, type InjectionKey, onBeforeUnmount, watchEffect, onMounted, type Ref, 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<TooltipContext>;
const tooltipStyles = css`
.tooltip-container {
display: inline-block;
}
.tooltip-carrier {
opacity: 0;
display: block;
pointer-events: none;
background-color: var(--dj-bgpalette1);
box-shadow: 0 0 12px 1px rgb(10 12 15 / 70%);
border: var(--dj-palette3) solid 1px;
color: var(--dj-palette3);
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<HTMLElement | null> }) {
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 () => <>
<div class="tooltip-container"
{...attrs}
onMouseenter={(e) => tooltip.show(props.tooltip, e.pageX, e.pageY)}
onMouseleave={() => tooltip.hide()}>
{slots.default?.()}
</div>
</>;
},
});