Files
dcalc/frontend-ui/src/components/ui/dialog/UiDialogContent.vue
T
2026-08-05 19:01:52 +08:00

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>