130 lines
2.9 KiB
Vue
130 lines
2.9 KiB
Vue
<script setup>
|
|
defineOptions({
|
|
name: "Modal",
|
|
});
|
|
|
|
const props = defineProps({
|
|
open: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
size: {
|
|
type: String,
|
|
default: 'xl',
|
|
validator: (value) => ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', 'full'].includes(value) || /^\d+px$/.test(value) || /^\d+rem$/.test(value),
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(["update:open"]);
|
|
const originalOverflow = ref("");
|
|
|
|
const modalSize = computed(() => {
|
|
const sizes = {
|
|
sm: '384px',
|
|
md: '448px',
|
|
lg: '512px',
|
|
xl: '576px',
|
|
'2xl': '672px',
|
|
'3xl': '768px',
|
|
'4xl': '896px',
|
|
'5xl': '1024px',
|
|
'full': '100%'
|
|
};
|
|
|
|
// If it's a predefined size, return the corresponding width
|
|
if (sizes[props.size]) {
|
|
return sizes[props.size];
|
|
}
|
|
|
|
// If it's a custom size (e.g., '600px' or '20rem'), return it directly
|
|
if (/^\d+px$/.test(props.size) || /^\d+rem$/.test(props.size)) {
|
|
return props.size;
|
|
}
|
|
|
|
// Default fallback
|
|
return sizes.xl;
|
|
});
|
|
|
|
const lockScroll = () => {
|
|
originalOverflow.value = document.body.style.overflow;
|
|
document.body.style.overflow = "hidden";
|
|
};
|
|
|
|
const unlockScroll = () => {
|
|
document.body.style.overflow = originalOverflow.value;
|
|
};
|
|
|
|
const close = () => {
|
|
emit("update:open", false);
|
|
};
|
|
|
|
// Watch for changes in open state to handle scroll locking
|
|
watch(
|
|
() => props.open,
|
|
(newValue) => {
|
|
if (newValue) {
|
|
lockScroll();
|
|
} else {
|
|
unlockScroll();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Close on escape key and handle scroll locking
|
|
onMounted(() => {
|
|
const handleEscape = (e) => {
|
|
if (e.key === "Escape" && props.open) close();
|
|
};
|
|
document.addEventListener("keydown", handleEscape);
|
|
|
|
// Initial scroll lock if modal is open on mount
|
|
if (props.open) {
|
|
lockScroll();
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener("keydown", handleEscape);
|
|
// Ensure we unlock scroll when component is destroyed
|
|
if (props.open) {
|
|
unlockScroll();
|
|
}
|
|
});
|
|
});
|
|
|
|
provide("closeModal", close);
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<Transition
|
|
enter-active-class="duration-200 ease-out"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="duration-150 ease-in"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div v-if="open" class="fixed inset-0 z-[9999]">
|
|
<!-- Backdrop -->
|
|
<div
|
|
class="fixed inset-0 bg-black/70"
|
|
@click="close"
|
|
/>
|
|
<!-- Modal Content -->
|
|
<div
|
|
class="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]"
|
|
>
|
|
<div class="w-full mx-auto px-4 sm:px-0" :style="{ width: modalSize }">
|
|
<div
|
|
class="relative rounded-lg border bg-background p-6 shadow-lg"
|
|
@click.stop
|
|
>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|