2025-04-05 22:40:40 +02:00

162 lines
4.8 KiB
Vue

<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center">
<div
ref="modalRef"
:style="{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
}"
class="bg-white rounded-2xl border-2 border-gray-300 shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col"
>
<!-- Header - Make it the drag handle -->
<div class="flex justify-between items-center p-6 border-b border-gray-100 cursor-move" @mousedown="startDrag" @touchstart="startDrag">
<h3 class="text-2xl font-semibold text-gray-900">{{ title }}</h3>
<button @click="close" class="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Body -->
<div class="p-6 flex-1 overflow-auto">
<slot></slot>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
const props = defineProps<{
isOpen: boolean;
title: string;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const modalRef = ref<HTMLElement | null>(null);
const position = ref({ x: 0, y: 0 });
const isDragging = ref(false);
const dragOffset = ref({ x: 0, y: 0 });
// Center the modal when it opens
const centerModal = () => {
if (!modalRef.value) return;
const modalRect = modalRef.value.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
position.value = {
x: (viewportWidth - modalRect.width) / 2,
y: (viewportHeight - modalRect.height) / 2,
};
};
const startDrag = (event: MouseEvent | TouchEvent) => {
if (!modalRef.value) return;
event.preventDefault();
isDragging.value = true;
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
dragOffset.value = {
x: clientX - position.value.x,
y: clientY - position.value.y,
};
document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag, { passive: false });
document.addEventListener('mouseup', stopDrag);
document.addEventListener('touchend', stopDrag);
};
const drag = (event: MouseEvent | TouchEvent) => {
if (!isDragging.value || !modalRef.value) return;
event.preventDefault();
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
const modalRect = modalRef.value.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Calculate new position
let newX = clientX - dragOffset.value.x;
let newY = clientY - dragOffset.value.y;
// Constrain to viewport bounds
newX = Math.max(0, Math.min(newX, viewportWidth - modalRect.width));
newY = Math.max(0, Math.min(newY, viewportHeight - modalRect.height));
position.value = { x: newX, y: newY };
};
const stopDrag = () => {
isDragging.value = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag);
};
const close = () => {
emit('close');
position.value = { x: 0, y: 0 };
};
// Handle ESC key to close modal
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.isOpen) {
close();
}
};
// Handle window resize
const handleResize = () => {
if (!isDragging.value) {
centerModal();
}
};
// Watch for modal opening
watch(
() => props.isOpen,
newValue => {
if (newValue) {
// Use nextTick to ensure the modal is mounted
Vue.nextTick(() => {
centerModal();
});
}
}
);
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
window.addEventListener('resize', handleResize);
if (props.isOpen) {
centerModal();
}
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('resize', handleResize);
document.removeEventListener('mousemove', drag);
document.removeEventListener('touchmove', drag);
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag);
});
</script>