1
0
forked from noxious/client

#231 : Remove logic that prevents modals from being dragged outside of the view & refactor modal TS

This commit is contained in:
Dennis Postma 2024-11-04 23:34:41 +01:00
parent 42539cc73d
commit 7b61f71fa9

View File

@ -1,22 +1,25 @@
<template> <template>
<Teleport to="body"> <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"> <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 @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"> <div class="relative z-10">
<slot name="modalHeader" /> <slot name="modalHeader" />
</div> </div>
<div class="flex gap-2.5"> <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"> <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'" draggable="false" :src="isFullScreen ? '/assets/icons/minimize.svg' : '/assets/icons/increase-size-option.svg'" class="w-3.5 h-3.5 invert" /> <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>
<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"> <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" draggable="false" src="/assets/icons/close-button-white.svg" class="w-full h-full" /> <img alt="close" src="/assets/icons/close-button-white.svg" class="w-full h-full" draggable="false" />
</button> </button>
</div> </div>
</div> </div>
<!-- Body -->
<div class="overflow-hidden grow relative"> <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"> <div class="relative z-10 h-full">
<slot name="modalBody" /> <slot name="modalBody" />
</div> </div>
@ -27,219 +30,187 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, onMounted, onUnmounted, ref, watch, computed } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const props = defineProps({ interface ModalProps {
isModalOpen: { isModalOpen: boolean
type: Boolean, closable?: boolean
default: false isResizable?: boolean
}, canFullScreen?: boolean
closable: { modalPositionX?: number
type: Boolean, modalPositionY?: number
default: true modalWidth?: number
}, modalHeight?: number
isResizable: { }
type: Boolean,
default: true interface Position {
}, x: number
canFullScreen: { y: number
type: Boolean, width: number
default: false height: number
}, }
modalPositionX: {
type: Number, const props = withDefaults(defineProps<ModalProps>(), {
default: 0 isModalOpen: false,
}, closable: true,
modalPositionY: { isResizable: true,
type: Number, canFullScreen: false,
default: 0 modalPositionX: 0,
}, modalPositionY: 0,
modalWidth: { modalWidth: 500,
type: Number, modalHeight: 280
default: 500
},
modalHeight: {
type: Number,
default: 280
}
}) })
const isModalOpenRef = ref(props.isModalOpen) const emit = defineEmits<{
const emit = defineEmits(['modal:close', 'character:create']) 'modal:close': []
'character:create': []
}>()
const isModalOpenRef = ref(props.isModalOpen)
const width = ref(props.modalWidth) const width = ref(props.modalWidth)
const height = ref(props.modalHeight) const height = ref(props.modalHeight)
const x = ref(0) const x = ref(0)
const y = ref(0) const y = ref(0)
const minWidth = ref(200)
const minHeight = ref(100)
const isResizing = ref(false) const isResizing = ref(false)
const isDragging = ref(false) const isDragging = ref(false)
const isFullScreen = ref(false) const isFullScreen = ref(false)
let startX = 0 const minDimensions = {
let startY = 0 width: 200,
let initialX = 0 height: 100
let initialY = 0 }
let startWidth = 0
let startHeight = 0 let dragState = {
let preFullScreenState = { x: 0, y: 0, width: 0, height: 0 } 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(() => ({ const modalStyle = computed(() => ({
borderRadius: isFullScreen.value ? '0' : '6px', borderRadius: isFullScreen.value ? '0' : '6px',
top: isFullScreen.value ? '0' : `${y.value}px`, top: isFullScreen.value ? '0' : `${y.value}px`,
left: isFullScreen.value ? '0' : `${x.value}px`, left: isFullScreen.value ? '0' : `${x.value}px`,
width: isFullScreen.value ? '100vw' : `${width.value}px`, width: isFullScreen.value ? '100vw' : `${width.value}px`,
height: isFullScreen.value ? '100vh' : `${height.value}px`, height: isFullScreen.value ? '100vh' : `${height.value}px`
maxWidth: '100vw',
maxHeight: '100vh'
})) }))
function close() {
emit('modal:close')
}
function startResize(event: MouseEvent) { function startResize(event: MouseEvent) {
if (isFullScreen.value) return if (isFullScreen.value) return
isResizing.value = true isResizing.value = true
startWidth = width.value - event.clientX dragState.startWidth = width.value - event.clientX
startHeight = height.value - event.clientY dragState.startHeight = height.value - event.clientY
event.preventDefault() event.preventDefault()
} }
function resizeModal(event: MouseEvent) { function resizeModal(event: MouseEvent) {
if (!isResizing.value || isFullScreen.value) return if (!isResizing.value || isFullScreen.value) return
const newWidth = Math.min(startWidth + event.clientX, window.innerWidth) width.value = Math.max(dragState.startWidth + event.clientX, minDimensions.width)
const newHeight = Math.min(startHeight + event.clientY, window.innerHeight) height.value = Math.max(dragState.startHeight + event.clientY, minDimensions.height)
width.value = Math.max(newWidth, minWidth.value)
height.value = Math.max(newHeight, minHeight.value)
adjustPosition()
}
function stopResize() {
isResizing.value = false
} }
function startDrag(event: MouseEvent) { function startDrag(event: MouseEvent) {
if (isFullScreen.value) return if (isFullScreen.value) return
isDragging.value = true isDragging.value = true
startX = event.clientX dragState = {
startY = event.clientY startX: event.clientX,
initialX = x.value startY: event.clientY,
initialY = y.value initialX: x.value,
initialY: y.value,
startWidth: width.value,
startHeight: height.value
}
event.preventDefault() event.preventDefault()
} }
function drag(event: MouseEvent) { function drag(event: MouseEvent) {
if (!isDragging.value || isFullScreen.value) return if (!isDragging.value || isFullScreen.value) return
const dx = event.clientX - startX x.value = dragState.initialX + (event.clientX - dragState.startX)
const dy = event.clientY - startY y.value = dragState.initialY + (event.clientY - dragState.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
}
} }
function toggleFullScreen() { function toggleFullScreen() {
if (isFullScreen.value) { if (isFullScreen.value) {
// Exit full-screen Object.assign({ x, y, width, height }, preFullScreenState)
x.value = preFullScreenState.x
y.value = preFullScreenState.y
width.value = preFullScreenState.width
height.value = preFullScreenState.height
isFullScreen.value = false
} else { } else {
// Enter full-screen
preFullScreenState = { x: x.value, y: y.value, width: width.value, height: height.value } 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( watch(
() => props.isModalOpen, () => props.isModalOpen,
(value) => { (value) => {
isModalOpenRef.value = value isModalOpenRef.value = value
if (value) { if (value) initializePosition()
initializePosition()
}
} }
) )
watch( watch(
() => props.modalWidth, () => props.modalWidth,
(value) => { (value) => (width.value = value)
width.value = Math.min(value, window.innerWidth)
}
) )
watch( watch(
() => props.modalHeight, () => props.modalHeight,
(value) => { (value) => (height.value = value)
height.value = Math.min(value, window.innerHeight)
}
) )
watch( watch(
() => props.modalPositionX, () => props.modalPositionX,
(value) => { (value) => (x.value = value)
x.value = value
}
) )
watch( watch(
() => props.modalPositionY, () => props.modalPositionY,
(value) => { (value) => (y.value = value)
y.value = value
}
) )
// Lifecycle hooks
onMounted(() => { onMounted(() => {
addEventListener('mousemove', drag) const handlers: Record<string, EventListener[]> = {
addEventListener('mouseup', stopDrag) mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
addEventListener('mousemove', resizeModal) mouseup: [
addEventListener('mouseup', stopResize) () => {
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) { isDragging.value = false
addEventListener('resize', handleResize) },
() => {
isResizing.value = false
} }
]
}
Object.entries(handlers).forEach(([event, fns]) => {
fns.forEach((fn) => window.addEventListener(event, fn))
})
initializePosition() initializePosition()
}) })
onUnmounted(() => { onUnmounted(() => {
removeEventListener('mousemove', drag) const handlers: Record<string, EventListener[]> = {
removeEventListener('mouseup', stopDrag) mousemove: [(e: Event) => drag(e as MouseEvent), (e: Event) => resizeModal(e as MouseEvent)],
removeEventListener('mousemove', resizeModal) mouseup: [
removeEventListener('mouseup', stopResize) () => {
if (props.modalPositionX !== 0 && props.modalPositionY !== 0) { isDragging.value = false
removeEventListener('resize', handleResize) },
() => {
isResizing.value = false
} }
]
}
Object.entries(handlers).forEach(([event, fns]) => {
fns.forEach((fn) => window.removeEventListener(event, fn))
})
}) })
</script> </script>