162 lines
4.8 KiB
Vue
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>
|