feat: snap floating add button to edge
This commit is contained in:
@@ -1,22 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { clampFabPosition, isFabDrag } from '../lib/mvp-utils'
|
||||
import { clampFabPosition, isFabDrag, snapFabPosition } from '../lib/mvp-utils'
|
||||
|
||||
const props = withDefaults(defineProps<{ label?: string }>(), { label: '添加' })
|
||||
const emit = defineEmits<{ activate: [origin: { x: number; y: number }] }>()
|
||||
const dragging = ref(false)
|
||||
const snapping = ref(false)
|
||||
const position = ref<{ x: number; y: number } | null>(null)
|
||||
const pointer = ref<{ id: number; startX: number; startY: number; originX: number; originY: number } | null>(null)
|
||||
let suppressClick = false
|
||||
let snappingTimer: number | undefined
|
||||
|
||||
const buttonStyle = computed(() => position.value
|
||||
? { left: `${position.value.x}px`, top: `${position.value.y}px`, right: 'auto', bottom: 'auto' }
|
||||
: {})
|
||||
|
||||
function clearSnappingTimer() {
|
||||
if (snappingTimer) {
|
||||
window.clearTimeout(snappingTimer)
|
||||
snappingTimer = undefined
|
||||
}
|
||||
}
|
||||
function startSnappingPulse() {
|
||||
clearSnappingTimer()
|
||||
snapping.value = true
|
||||
snappingTimer = window.setTimeout(() => {
|
||||
snapping.value = false
|
||||
snappingTimer = undefined
|
||||
}, 220)
|
||||
}
|
||||
function snapFabToCurrentViewport() {
|
||||
if (!position.value || dragging.value) return
|
||||
position.value = snapFabPosition(position.value.x, position.value.y, window.innerWidth, window.innerHeight, 56, 14, 84)
|
||||
}
|
||||
function startDrag(event: PointerEvent) {
|
||||
clearSnappingTimer()
|
||||
snapping.value = false
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
position.value = clampFabPosition(rect.left, rect.top, window.innerWidth, window.innerHeight, 56, 14, 84)
|
||||
pointer.value = { id: event.pointerId, startX: event.clientX, startY: event.clientY, originX: rect.left, originY: rect.top }
|
||||
try { target.setPointerCapture?.(event.pointerId) } catch { /* pointer capture unavailable in synthetic events */ }
|
||||
}
|
||||
@@ -34,21 +57,30 @@ function finishDrag(event: PointerEvent) {
|
||||
if (!start || start.id !== event.pointerId) return
|
||||
const dragged = dragging.value || isFabDrag(event.clientX - start.startX, event.clientY - start.startY)
|
||||
pointer.value = null
|
||||
dragging.value = false
|
||||
if (dragged) {
|
||||
const current = position.value ?? clampFabPosition(start.originX, start.originY, window.innerWidth, window.innerHeight, 56, 14, 84)
|
||||
position.value = snapFabPosition(current.x, current.y, window.innerWidth, window.innerHeight, 56, 14, 84)
|
||||
startSnappingPulse()
|
||||
suppressClick = true
|
||||
window.setTimeout(() => { suppressClick = false }, 180)
|
||||
}
|
||||
dragging.value = false
|
||||
}
|
||||
function activate(event: MouseEvent) {
|
||||
if (suppressClick) return
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
emit('activate', { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 })
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('resize', snapFabToCurrentViewport))
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', snapFabToCurrentViewport)
|
||||
clearSnappingTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="unified-fab" :class="{ dragging }" :style="buttonStyle" :aria-label="props.label" @pointerdown="startDrag" @pointermove="moveDrag" @pointerup="finishDrag" @pointercancel="finishDrag" @click="activate">
|
||||
<button class="unified-fab" :class="{ dragging, snapping }" :style="buttonStyle" :aria-label="props.label" @pointerdown="startDrag" @pointermove="moveDrag" @pointerup="finishDrag" @pointercancel="finishDrag" @click="activate">
|
||||
<Plus />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user