1
0
forked from noxious/client

234 lines
6.6 KiB
Vue

<template>
<Teleport to="body">
<div v-if="isModalOpenRef" class="fixed border-solid border-2 border-gray-500 z-50 flex flex-col backdrop-blur-sm shadow-lg" :style="modalStyle">
<!-- Header -->
<div @mousedown="startDrag" class="cursor-move p-2.5 flex justify-between items-center border-solid border-0 border-b border-gray-500 relative">
<div
:class="{
'bg-[url(/assets/ui-texture.png)] bg-no-repeat bg-center bg-cover opacity-90': bgStyle === 'textured',
'bg-gray': bgStyle !== 'textured'
}"
class="rounded-t absolute w-full h-full top-0 left-0"
/>
<div class="relative z-10">
<slot name="modalHeader" />
</div>
<div class="flex gap-2.5">
<button v-if="canFullScreen" @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out">
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" :src="isFullScreen ? '/assets/icons/modal/minimize.svg' : '/assets/icons/modal/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
</button>
<button v-if="closable" @click="emit('modal:close')" class="w-3.5 h-3.5 m-0 p-0 relative hover:rotate-180 transition-transform duration-300 ease-in-out">
<img alt="close" src="/assets/icons/modal/close-button-white.svg" class="w-full h-full" draggable="false" />
</button>
</div>
</div>
<!-- Body -->
<div class="overflow-hidden grow relative">
<div
:class="{
'bg-[url(/assets/ui-texture.png)] bg-no-repeat bg-center bg-cover opacity-90': bgStyle === 'textured',
'bg-gray-800': bgStyle === 'dark',
'bg-gray': bgStyle === 'none'
}"
class="rounded-b absolute w-full h-full top-0 left-0"
/>
<div class="relative z-10 h-full">
<slot name="modalBody" />
</div>
<img v-if="isResizable && !isFullScreen" src="/assets/icons/modal/resize-icon.svg" alt="resize" class="absolute z-10 bottom-0 right-0 w-5 h-5 cursor-nwse-resize" @mousedown="startResize" />
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
interface ModalProps {
isModalOpen: boolean
closable?: boolean
isResizable?: boolean
isFullScreen?: boolean
canFullScreen?: boolean
modalPositionX?: number
modalPositionY?: number
modalWidth?: number
modalHeight?: number
bgStyle?: string
}
interface Position {
x: number
y: number
width: number
height: number
}
const props = withDefaults(defineProps<ModalProps>(), {
isModalOpen: false,
closable: true,
isResizable: true,
isFullScreen: false,
canFullScreen: false,
modalPositionX: 0,
modalPositionY: 0,
modalWidth: 500,
modalHeight: 280,
bgStyle: 'textured'
})
const emit = defineEmits<{
'modal:close': []
'character:create': []
}>()
const isModalOpenRef = ref(props.isModalOpen)
const width = ref(props.modalWidth)
const height = ref(props.modalHeight)
const x = ref(0)
const y = ref(0)
const isResizing = ref(false)
const isDragging = ref(false)
const isFullScreen = ref(props.isFullScreen)
const minDimensions = {
width: 200,
height: 100
}
let dragState = {
startX: 0,
startY: 0,
initialX: 0,
initialY: 0,
startWidth: 0,
startHeight: 0
}
let preFullScreenState: Position = { x: 0, y: 0, width: 0, height: 0 }
const modalStyle = computed(() => ({
borderRadius: isFullScreen.value ? '0' : '6px',
top: isFullScreen.value ? '0' : `${y.value}px`,
left: isFullScreen.value ? '0' : `${x.value}px`,
width: isFullScreen.value ? '100vw' : `${width.value}px`,
height: isFullScreen.value ? '100vh' : `${height.value}px`
}))
function startResize(event: MouseEvent) {
if (isFullScreen.value) return
isResizing.value = true
dragState.startWidth = width.value - event.clientX
dragState.startHeight = height.value - event.clientY
event.preventDefault()
}
function resizeModal(event: MouseEvent) {
if (!isResizing.value || isFullScreen.value) return
width.value = Math.max(dragState.startWidth + event.clientX, minDimensions.width)
height.value = Math.max(dragState.startHeight + event.clientY, minDimensions.height)
}
function startDrag(event: MouseEvent) {
if (isFullScreen.value) return
isDragging.value = true
dragState = {
startX: event.clientX,
startY: event.clientY,
initialX: x.value,
initialY: y.value,
startWidth: width.value,
startHeight: height.value
}
event.preventDefault()
}
function drag(event: MouseEvent) {
if (!isDragging.value || isFullScreen.value) return
x.value = dragState.initialX + (event.clientX - dragState.startX)
y.value = dragState.initialY + (event.clientY - dragState.startY)
}
function toggleFullScreen() {
if (isFullScreen.value) {
Object.assign({ x, y, width, height }, preFullScreenState)
} else {
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
}
isFullScreen.value = !isFullScreen.value
}
function initializePosition() {
width.value = props.modalWidth
height.value = props.modalHeight
x.value = props.modalPositionX || (window.innerWidth - width.value) / 2
y.value = props.modalPositionY || (window.innerHeight - height.value) / 2
}
// Watchers
watch(
() => props.isModalOpen,
(value) => {
isModalOpenRef.value = value
if (value) initializePosition()
}
)
watch(
() => props.modalWidth,
(value) => (width.value = value)
)
watch(
() => props.modalHeight,
(value) => (height.value = value)
)
watch(
() => props.modalPositionX,
(value) => (x.value = value)
)
watch(
() => props.modalPositionY,
(value) => (y.value = value)
)
// Lifecycle hooks
onMounted(() => {
const handlers: Record<string, EventListener[]> = {
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
mouseup: [
() => {
isDragging.value = false
},
() => {
isResizing.value = false
}
]
}
Object.entries(handlers).forEach(([event, fns]) => {
fns.forEach((fn) => window.addEventListener(event, fn))
})
initializePosition()
})
onUnmounted(() => {
const handlers: Record<string, EventListener[]> = {
mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
mouseup: [
() => {
isDragging.value = false
},
() => {
isResizing.value = false
}
]
}
Object.entries(handlers).forEach(([event, fns]) => {
fns.forEach((fn) => window.removeEventListener(event, fn))
})
})
</script>