142 lines
4.3 KiB
Vue
142 lines
4.3 KiB
Vue
<template>
|
|
<div class="relative inline-block">
|
|
<div @mouseenter="showTooltip" @mouseleave="hideTooltip">
|
|
<slot></slot>
|
|
</div>
|
|
<div v-show="isVisible" ref="tooltipEl" class="tooltip fixed z-50 px-2 py-1 text-xs font-medium text-white bg-gray-800 rounded shadow-lg whitespace-nowrap" :style="tooltipStyle">
|
|
{{ text }}
|
|
<div class="absolute w-2 h-2 bg-gray-800 transform rotate-45" :style="arrowStyle"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
|
|
|
const props = defineProps<{
|
|
text: string;
|
|
position?: 'top' | 'right' | 'bottom' | 'left';
|
|
offset?: number;
|
|
}>();
|
|
|
|
const isVisible = ref(false);
|
|
const tooltipEl = ref<HTMLElement | null>(null);
|
|
const position = computed(() => props.position || 'top');
|
|
const offset = computed(() => props.offset || 8);
|
|
|
|
// Dynamic styles for tooltip and arrow
|
|
const tooltipStyle = ref({
|
|
left: '0px',
|
|
top: '0px',
|
|
});
|
|
|
|
const arrowStyle = ref({
|
|
left: '50%',
|
|
top: '100%',
|
|
transform: 'translate(-50%, -50%) rotate(45deg)',
|
|
});
|
|
|
|
const updatePosition = (event: MouseEvent) => {
|
|
if (!isVisible.value || !tooltipEl.value) return;
|
|
|
|
const tooltip = tooltipEl.value;
|
|
const tooltipRect = tooltip.getBoundingClientRect();
|
|
const padding = 10; // Padding from screen edges
|
|
|
|
let left = event.clientX;
|
|
let top = event.clientY;
|
|
|
|
// Calculate positions based on available space
|
|
const spaceAbove = top;
|
|
const spaceBelow = window.innerHeight - top;
|
|
const spaceLeft = left;
|
|
const spaceRight = window.innerWidth - left;
|
|
|
|
// Determine best position
|
|
let finalPosition = position.value;
|
|
if (finalPosition === 'top' && spaceAbove < tooltipRect.height + padding) {
|
|
finalPosition = spaceBelow > tooltipRect.height + padding ? 'bottom' : 'right';
|
|
} else if (finalPosition === 'bottom' && spaceBelow < tooltipRect.height + padding) {
|
|
finalPosition = spaceAbove > tooltipRect.height + padding ? 'top' : 'right';
|
|
} else if (finalPosition === 'left' && spaceLeft < tooltipRect.width + padding) {
|
|
finalPosition = spaceRight > tooltipRect.width + padding ? 'right' : 'top';
|
|
} else if (finalPosition === 'right' && spaceRight < tooltipRect.width + padding) {
|
|
finalPosition = spaceLeft > tooltipRect.width + padding ? 'left' : 'top';
|
|
}
|
|
|
|
// Position tooltip based on final position
|
|
switch (finalPosition) {
|
|
case 'top':
|
|
left -= tooltipRect.width / 2;
|
|
top -= tooltipRect.height + offset.value;
|
|
arrowStyle.value = {
|
|
left: '50%',
|
|
top: '100%',
|
|
transform: 'translate(-50%, -50%) rotate(45deg)',
|
|
};
|
|
break;
|
|
case 'bottom':
|
|
left -= tooltipRect.width / 2;
|
|
top += offset.value;
|
|
arrowStyle.value = {
|
|
left: '50%',
|
|
top: '0',
|
|
transform: 'translate(-50%, -50%) rotate(45deg)',
|
|
};
|
|
break;
|
|
case 'left':
|
|
left -= tooltipRect.width + offset.value;
|
|
top -= tooltipRect.height / 2;
|
|
arrowStyle.value = {
|
|
left: '100%',
|
|
top: '50%',
|
|
transform: 'translate(-50%, -50%) rotate(45deg)',
|
|
};
|
|
break;
|
|
case 'right':
|
|
left += offset.value;
|
|
top -= tooltipRect.height / 2;
|
|
arrowStyle.value = {
|
|
left: '0',
|
|
top: '50%',
|
|
transform: 'translate(-50%, -50%) rotate(45deg)',
|
|
};
|
|
break;
|
|
}
|
|
|
|
// Ensure tooltip stays within screen bounds
|
|
left = Math.max(padding, Math.min(left, window.innerWidth - tooltipRect.width - padding));
|
|
top = Math.max(padding, Math.min(top, window.innerHeight - tooltipRect.height - padding));
|
|
|
|
tooltipStyle.value = {
|
|
left: `${left}px`,
|
|
top: `${top}px`,
|
|
};
|
|
};
|
|
|
|
const showTooltip = (event: MouseEvent) => {
|
|
isVisible.value = true;
|
|
// Wait for next tick to ensure tooltip is rendered
|
|
setTimeout(() => updatePosition(event), 0);
|
|
};
|
|
|
|
const hideTooltip = () => {
|
|
isVisible.value = false;
|
|
};
|
|
|
|
// Track mouse movement when tooltip is visible
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
if (isVisible.value) {
|
|
updatePosition(event);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('mousemove', handleMouseMove);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
});
|
|
</script>
|