146 lines
4.3 KiB
Vue
146 lines
4.3 KiB
Vue
<script setup lang="ts">
|
|
import type { CSSProperties, HTMLAttributes } from 'vue'
|
|
import {
|
|
DialogContent,
|
|
type DialogContentEmits,
|
|
type DialogContentProps,
|
|
useForwardPropsEmits,
|
|
} from 'reka-ui'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const props = withDefaults(
|
|
defineProps<
|
|
DialogContentProps & {
|
|
class?: HTMLAttributes['class']
|
|
draggable?: boolean
|
|
}
|
|
>(),
|
|
{ draggable: true },
|
|
)
|
|
const emits = defineEmits<DialogContentEmits>()
|
|
const dragOffset = ref({ x: 0, y: 0 })
|
|
let activePointerId: number | undefined
|
|
let dragOrigin = { clientX: 0, clientY: 0, x: 0, y: 0 }
|
|
let dragBounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 }
|
|
|
|
const delegatedProps = computed(() => {
|
|
const { class: _class, draggable: _draggable, ...delegated } = props
|
|
return delegated
|
|
})
|
|
|
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
const dragStyle = computed<CSSProperties>(() => ({
|
|
'--ui-dialog-drag-x': `${dragOffset.value.x}px`,
|
|
'--ui-dialog-drag-y': `${dragOffset.value.y}px`,
|
|
}))
|
|
|
|
function clamp(value: number, min: number, max: number) {
|
|
return Math.min(Math.max(value, min), max)
|
|
}
|
|
|
|
function stopDragging() {
|
|
activePointerId = undefined
|
|
document.documentElement.classList.remove('is-dialog-dragging')
|
|
window.removeEventListener('pointermove', handlePointerMove)
|
|
window.removeEventListener('pointerup', stopDragging)
|
|
window.removeEventListener('pointercancel', stopDragging)
|
|
}
|
|
|
|
function resetPosition() {
|
|
stopDragging()
|
|
dragOffset.value = { x: 0, y: 0 }
|
|
}
|
|
|
|
function handlePointerMove(event: PointerEvent) {
|
|
if (activePointerId !== event.pointerId) return
|
|
const deltaX = event.clientX - dragOrigin.clientX
|
|
const deltaY = event.clientY - dragOrigin.clientY
|
|
dragOffset.value = {
|
|
x: dragOrigin.x + clamp(deltaX, dragBounds.minX, dragBounds.maxX),
|
|
y: dragOrigin.y + clamp(deltaY, dragBounds.minY, dragBounds.maxY),
|
|
}
|
|
}
|
|
|
|
function handlePointerDown(event: PointerEvent) {
|
|
if (!props.draggable || event.button !== 0) return
|
|
const target = event.target
|
|
if (!(target instanceof Element)) return
|
|
const handle = target.closest<HTMLElement>('[data-ui-dialog-drag-handle]')
|
|
if (!handle || target.closest('button, a, input, textarea, select, [data-ui-dialog-no-drag]'))
|
|
return
|
|
|
|
const content = event.currentTarget
|
|
if (!(content instanceof HTMLElement)) return
|
|
const rect = content.getBoundingClientRect()
|
|
const viewportMargin = 8
|
|
activePointerId = event.pointerId
|
|
dragOrigin = {
|
|
clientX: event.clientX,
|
|
clientY: event.clientY,
|
|
x: dragOffset.value.x,
|
|
y: dragOffset.value.y,
|
|
}
|
|
dragBounds = {
|
|
minX: viewportMargin - rect.left,
|
|
maxX: window.innerWidth - viewportMargin - rect.right,
|
|
minY: viewportMargin - rect.top,
|
|
maxY: window.innerHeight - viewportMargin - rect.bottom,
|
|
}
|
|
document.documentElement.classList.add('is-dialog-dragging')
|
|
window.addEventListener('pointermove', handlePointerMove)
|
|
window.addEventListener('pointerup', stopDragging)
|
|
window.addEventListener('pointercancel', stopDragging)
|
|
event.preventDefault()
|
|
}
|
|
|
|
onMounted(() => window.addEventListener('resize', resetPosition))
|
|
onBeforeUnmount(() => {
|
|
stopDragging()
|
|
window.removeEventListener('resize', resetPosition)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<DialogContent
|
|
v-bind="forwarded"
|
|
:style="dragStyle"
|
|
:data-draggable="draggable ? '' : undefined"
|
|
@pointerdown="handlePointerDown"
|
|
@open-auto-focus="resetPosition"
|
|
:class="
|
|
cn(
|
|
'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
|
|
props.class,
|
|
)
|
|
"
|
|
>
|
|
<slot />
|
|
</DialogContent>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.ui-dialog-content {
|
|
width: min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size));
|
|
max-height: calc(100dvh - 16px);
|
|
overflow: hidden;
|
|
border: 1px solid var(--ui-dialog-border);
|
|
border-radius: 0;
|
|
background: var(--ui-dialog-background);
|
|
box-shadow:
|
|
0 18px 38px var(--ui-dialog-shadow),
|
|
inset 0 0 0 1px var(--ui-dialog-inset);
|
|
color: var(--ui-surface-foreground);
|
|
transform: translate(
|
|
calc(-50% + var(--ui-dialog-drag-x, 0px)),
|
|
calc(-50% + var(--ui-dialog-drag-y, 0px))
|
|
);
|
|
will-change: transform;
|
|
}
|
|
|
|
:global(.is-dialog-dragging),
|
|
:global(.is-dialog-dragging *) {
|
|
cursor: grabbing !important;
|
|
user-select: none !important;
|
|
}
|
|
</style>
|