#231 : Remove logic that prevents modals from being dragged outside of the view & refactor modal TS
This commit is contained in:
parent
42539cc73d
commit
7b61f71fa9
@ -1,22 +1,25 @@
|
||||
<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="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90"></div>
|
||||
<div class="rounded-t absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-center bg-cover opacity-90" />
|
||||
<div class="relative z-10">
|
||||
<slot name="modalHeader" />
|
||||
</div>
|
||||
<div class="flex gap-2.5">
|
||||
<button @click="toggleFullScreen" class="w-5 h-5 m-0 p-0 relative hover:scale-110 transition-transform duration-300 ease-in-out" v-if="canFullScreen">
|
||||
<img :alt="isFullScreen ? 'exit full-screen' : 'full-screen'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" />
|
||||
<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/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" draggable="false" />
|
||||
</button>
|
||||
<button @click="close" v-if="closable" 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" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" />
|
||||
<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/close-button-white.svg" class="w-full h-full" draggable="false" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="overflow-hidden grow relative">
|
||||
<div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90"></div>
|
||||
<div class="rounded-b absolute w-full h-full top-0 left-0 bg-[url('/assets/ui-texture.png')] bg-no-repeat bg-cover bg-center opacity-90" />
|
||||
<div class="relative z-10 h-full">
|
||||
<slot name="modalBody" />
|
||||
</div>
|
||||
@ -27,219 +30,187 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isModalOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isResizable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
canFullScreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modalPositionX: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
modalPositionY: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
modalWidth: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
modalHeight: {
|
||||
type: Number,
|
||||
default: 280
|
||||
interface ModalProps {
|
||||
isModalOpen: boolean
|
||||
closable?: boolean
|
||||
isResizable?: boolean
|
||||
canFullScreen?: boolean
|
||||
modalPositionX?: number
|
||||
modalPositionY?: number
|
||||
modalWidth?: number
|
||||
modalHeight?: number
|
||||
}
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ModalProps>(), {
|
||||
isModalOpen: false,
|
||||
closable: true,
|
||||
isResizable: true,
|
||||
canFullScreen: false,
|
||||
modalPositionX: 0,
|
||||
modalPositionY: 0,
|
||||
modalWidth: 500,
|
||||
modalHeight: 280
|
||||
})
|
||||
|
||||
const isModalOpenRef = ref(props.isModalOpen)
|
||||
const emit = defineEmits(['modal:close', 'character:create'])
|
||||
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 minWidth = ref(200)
|
||||
const minHeight = ref(100)
|
||||
const isResizing = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const isFullScreen = ref(false)
|
||||
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let initialX = 0
|
||||
let initialY = 0
|
||||
let startWidth = 0
|
||||
let startHeight = 0
|
||||
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 }
|
||||
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`,
|
||||
maxWidth: '100vw',
|
||||
maxHeight: '100vh'
|
||||
height: isFullScreen.value ? '100vh' : `${height.value}px`
|
||||
}))
|
||||
|
||||
function close() {
|
||||
emit('modal:close')
|
||||
}
|
||||
|
||||
function startResize(event: MouseEvent) {
|
||||
if (isFullScreen.value) return
|
||||
isResizing.value = true
|
||||
startWidth = width.value - event.clientX
|
||||
startHeight = height.value - event.clientY
|
||||
dragState.startWidth = width.value - event.clientX
|
||||
dragState.startHeight = height.value - event.clientY
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function resizeModal(event: MouseEvent) {
|
||||
if (!isResizing.value || isFullScreen.value) return
|
||||
const newWidth = Math.min(startWidth + event.clientX, window.innerWidth)
|
||||
const newHeight = Math.min(startHeight + event.clientY, window.innerHeight)
|
||||
width.value = Math.max(newWidth, minWidth.value)
|
||||
height.value = Math.max(newHeight, minHeight.value)
|
||||
adjustPosition()
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
isResizing.value = false
|
||||
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
|
||||
startX = event.clientX
|
||||
startY = event.clientY
|
||||
initialX = x.value
|
||||
initialY = y.value
|
||||
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
|
||||
const dx = event.clientX - startX
|
||||
const dy = event.clientY - startY
|
||||
x.value = initialX + dx
|
||||
y.value = initialY + dy
|
||||
adjustPosition()
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
function adjustPosition() {
|
||||
if (isFullScreen.value) return
|
||||
x.value = Math.min(x.value, window.innerWidth - width.value)
|
||||
y.value = Math.min(y.value, window.innerHeight - height.value)
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (isFullScreen.value) return
|
||||
width.value = Math.min(width.value, window.innerWidth)
|
||||
height.value = Math.min(height.value, window.innerHeight)
|
||||
adjustPosition()
|
||||
}
|
||||
|
||||
function initializePosition() {
|
||||
width.value = Math.min(props.modalWidth, window.innerWidth)
|
||||
height.value = Math.min(props.modalHeight, window.innerHeight)
|
||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
||||
x.value = props.modalPositionX
|
||||
y.value = props.modalPositionY
|
||||
} else {
|
||||
x.value = (window.innerWidth - width.value) / 2
|
||||
y.value = (window.innerHeight - height.value) / 2
|
||||
}
|
||||
x.value = dragState.initialX + (event.clientX - dragState.startX)
|
||||
y.value = dragState.initialY + (event.clientY - dragState.startY)
|
||||
}
|
||||
|
||||
function toggleFullScreen() {
|
||||
if (isFullScreen.value) {
|
||||
// Exit full-screen
|
||||
x.value = preFullScreenState.x
|
||||
y.value = preFullScreenState.y
|
||||
width.value = preFullScreenState.width
|
||||
height.value = preFullScreenState.height
|
||||
isFullScreen.value = false
|
||||
Object.assign({ x, y, width, height }, preFullScreenState)
|
||||
} else {
|
||||
// Enter full-screen
|
||||
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value }
|
||||
isFullScreen.value = true
|
||||
}
|
||||
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()
|
||||
}
|
||||
if (value) initializePosition()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalWidth,
|
||||
(value) => {
|
||||
width.value = Math.min(value, window.innerWidth)
|
||||
}
|
||||
(value) => (width.value = value)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalHeight,
|
||||
(value) => {
|
||||
height.value = Math.min(value, window.innerHeight)
|
||||
}
|
||||
(value) => (height.value = value)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalPositionX,
|
||||
(value) => {
|
||||
x.value = value
|
||||
}
|
||||
(value) => (x.value = value)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modalPositionY,
|
||||
(value) => {
|
||||
y.value = value
|
||||
}
|
||||
(value) => (y.value = value)
|
||||
)
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
addEventListener('mousemove', drag)
|
||||
addEventListener('mouseup', stopDrag)
|
||||
addEventListener('mousemove', resizeModal)
|
||||
addEventListener('mouseup', stopResize)
|
||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
||||
addEventListener('resize', handleResize)
|
||||
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(() => {
|
||||
removeEventListener('mousemove', drag)
|
||||
removeEventListener('mouseup', stopDrag)
|
||||
removeEventListener('mousemove', resizeModal)
|
||||
removeEventListener('mouseup', stopResize)
|
||||
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) {
|
||||
removeEventListener('resize', handleResize)
|
||||
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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user