init
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { CSSProperties, HTMLAttributes } from 'vue'
|
||||
import {
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
@@ -8,23 +8,108 @@ import {
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
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, ...delegated } = props
|
||||
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 -translate-x-1/2 -translate-y-1/2 outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
|
||||
'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,
|
||||
)
|
||||
"
|
||||
@@ -36,6 +121,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<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;
|
||||
@@ -44,5 +130,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
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>
|
||||
|
||||
@@ -28,7 +28,11 @@ onBeforeUnmount(() => clearInterval(timer))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui-dialog-header" :class="{ 'is-focused': focused, 'is-unfocused': !focused }">
|
||||
<div
|
||||
class="ui-dialog-header"
|
||||
:class="{ 'is-focused': focused, 'is-unfocused': !focused }"
|
||||
data-ui-dialog-drag-handle
|
||||
>
|
||||
<img
|
||||
v-if="focused && effectFrames.length"
|
||||
class="ui-dialog-header__effect"
|
||||
@@ -60,6 +64,9 @@ onBeforeUnmount(() => clearInterval(timer))
|
||||
background: var(--ui-dialog-titlebar-background);
|
||||
color: var(--ui-titlebar-foreground);
|
||||
text-shadow: 0 1px 1px var(--ui-dialog-titlebar-text-shadow);
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.ui-dialog-header.is-focused {
|
||||
@@ -87,6 +94,10 @@ onBeforeUnmount(() => clearInterval(timer))
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
grid-column: 2;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-dialog-header__actions {
|
||||
|
||||
@@ -18,9 +18,15 @@ const delegatedProps = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.ui-dialog-title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--ui-titlebar-foreground);
|
||||
font-size: var(--ui-control-font-size-default);
|
||||
font-weight: 400;
|
||||
line-height: var(--ui-control-line-height);
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user