mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 12:03:25 -04:00
142 lines
3.6 KiB
Vue
142 lines
3.6 KiB
Vue
<script setup lang="ts">
|
|
import { arrow, autoUpdate, flip, shift, size, useFloating } from '@floating-ui/vue';
|
|
import type { Placement } from '@floating-ui/vue';
|
|
import type { CSSProperties } from 'vue';
|
|
|
|
const props = withDefaults(defineProps<{
|
|
tag?: string;
|
|
placement?: Placement;
|
|
resize?: boolean;
|
|
}>(), {
|
|
tag: 'span',
|
|
placement: 'bottom',
|
|
});
|
|
|
|
const reference = useTemplateRef<HTMLSpanElement>('reference');
|
|
const floating = useTemplateRef<HTMLSpanElement>('floating');
|
|
const floatingArrow = useTemplateRef<HTMLSpanElement>('floatingArrow');
|
|
const content = useTemplateRef<HTMLDivElement>('content');
|
|
|
|
const middleware = computed(() => {
|
|
const middleware = [flip(), shift()];
|
|
if (props.resize) {
|
|
middleware.push(size({
|
|
apply: ({ availableWidth, availableHeight }) => {
|
|
if (!content.value) {
|
|
return;
|
|
}
|
|
Object.assign(content.value.style, {
|
|
maxWidth: `${availableWidth}px`,
|
|
maxHeight: `${availableHeight}px`,
|
|
});
|
|
},
|
|
}));
|
|
}
|
|
middleware.push(arrow({ element: floatingArrow }));
|
|
return middleware;
|
|
});
|
|
|
|
const { floatingStyles, placement: calculatedPlacement, middlewareData } = useFloating(reference, floating, {
|
|
middleware,
|
|
placement: props.placement,
|
|
whileElementsMounted: autoUpdate,
|
|
});
|
|
|
|
const visible = ref(false);
|
|
|
|
const slots = defineSlots<{
|
|
default(): VNode;
|
|
content(): VNode[];
|
|
}>();
|
|
const hasContent = computed((): boolean => {
|
|
return !!slots.content()[0];
|
|
});
|
|
|
|
const arrowStyles = computed((): CSSProperties | undefined => {
|
|
const arrowData = middlewareData.value.arrow;
|
|
|
|
const staticSite = {
|
|
top: 'bottom',
|
|
right: 'left',
|
|
bottom: 'top',
|
|
left: 'right',
|
|
}[calculatedPlacement.value.split('-')[0]]!;
|
|
|
|
return {
|
|
left: typeof arrowData?.x === 'number' ? `${arrowData.x}px` : '',
|
|
top: typeof arrowData?.y === 'number' ? `${arrowData.y}px` : '',
|
|
[staticSite]: 0,
|
|
};
|
|
});
|
|
|
|
const hoverHandle = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const onEnter = () => {
|
|
visible.value = true;
|
|
if (hoverHandle.value) {
|
|
clearTimeout(hoverHandle.value);
|
|
hoverHandle.value = null;
|
|
}
|
|
};
|
|
const onLeave = () => {
|
|
hoverHandle.value = setTimeout(() => {
|
|
visible.value = false;
|
|
hoverHandle.value = null;
|
|
}, 50);
|
|
};
|
|
|
|
onMounted(() => {
|
|
// remove title from reference element to prevent a double tooltip
|
|
reference.value?.removeAttribute('title');
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<component
|
|
:is="tag"
|
|
v-bind="$attrs"
|
|
ref="reference"
|
|
@mouseenter="onEnter()"
|
|
@mouseleave="onLeave()"
|
|
>
|
|
<slot></slot>
|
|
</component>
|
|
<Teleport to="#teleports">
|
|
<div
|
|
v-if="visible && hasContent"
|
|
ref="floating"
|
|
class="popover p-1"
|
|
:style="floatingStyles"
|
|
@mouseenter="onEnter()"
|
|
@mouseleave="onLeave()"
|
|
>
|
|
<span
|
|
ref="floatingArrow"
|
|
class="popover-arrow bg-dark text-white"
|
|
:style="arrowStyles"
|
|
></span>
|
|
<div
|
|
ref="content"
|
|
class="overflow-auto bg-dark text-white px-2 py-1 rounded"
|
|
>
|
|
<slot name="content"></slot>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.popover {
|
|
font-weight: normal;
|
|
font-style: normal;
|
|
font-size: .85rem;
|
|
z-index: 999;
|
|
}
|
|
.popover-arrow {
|
|
position: absolute;
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
transform: rotate(45deg);
|
|
}
|
|
</style>
|