thats-me/frontend/_src/composables/usePanelDrag.js
2026-04-22 12:57:10 +02:00

137 lines
3.3 KiB
JavaScript

import { ref, onBeforeUnmount } from 'vue'
/**
* Composable for draggable bottom-sheet panels with snap points.
*
* Snap stops (in dvh): 100, 75, 50
* Close threshold: below 25dvh
*
* @param {Function} onClose - called when panel is dragged below threshold
* @returns {{ panelHeight, handleListeners, resetHeight }}
*/
export function usePanelDrag(onClose) {
const SNAP_POINTS = [100, 75, 50, 25] // dvh values
const CLOSE_THRESHOLD = 15 // below this → close
// Current panel height in dvh (null = use CSS default)
const panelHeight = ref(null)
const isDragging = ref(false)
let dragging = false
let startY = 0
let startHeight = 0
function getViewportHeight() {
return window.innerHeight
}
function pxToDvh(px) {
return (px / getViewportHeight()) * 100
}
function findNearestSnap(dvh) {
let nearest = SNAP_POINTS[0]
let minDist = Infinity
for (const snap of SNAP_POINTS) {
const dist = Math.abs(dvh - snap)
if (dist < minDist) {
minDist = dist
nearest = snap
}
}
return nearest
}
function onPointerDown(e) {
// Only primary button / single touch
if (e.button && e.button !== 0) return
dragging = true
isDragging.value = true
const clientY = e.touches ? e.touches[0].clientY : e.clientY
startY = clientY
// Current height: if panelHeight is set use it, else measure from CSS
const currentDvh = panelHeight.value ?? 75
startHeight = currentDvh
document.addEventListener('pointermove', onPointerMove, { passive: false })
document.addEventListener('pointerup', onPointerUp)
document.addEventListener('touchmove', onTouchMove, { passive: false })
document.addEventListener('touchend', onTouchEnd)
// Prevent text selection
e.preventDefault()
}
function onPointerMove(e) {
if (!dragging) return
const clientY = e.clientY
handleMove(clientY)
}
function onTouchMove(e) {
if (!dragging) return
if (e.touches.length !== 1) return
handleMove(e.touches[0].clientY)
e.preventDefault()
}
function handleMove(clientY) {
const deltaY = clientY - startY
const deltaDvh = pxToDvh(deltaY)
const newHeight = Math.max(10, Math.min(100, startHeight - deltaDvh))
panelHeight.value = newHeight
}
function onPointerUp() {
finishDrag()
}
function onTouchEnd() {
finishDrag()
}
function finishDrag() {
if (!dragging) return
dragging = false
isDragging.value = false
cleanup()
const currentHeight = panelHeight.value ?? 75
if (currentHeight < CLOSE_THRESHOLD) {
panelHeight.value = null
onClose()
} else {
// Snap to nearest point
panelHeight.value = findNearestSnap(currentHeight)
}
}
function cleanup() {
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
document.removeEventListener('touchmove', onTouchMove)
document.removeEventListener('touchend', onTouchEnd)
}
function resetHeight() {
panelHeight.value = null
}
onBeforeUnmount(cleanup)
// Event listeners to bind on the handle element
const handleListeners = {
pointerdown: onPointerDown,
touchstart: onPointerDown,
}
return {
panelHeight,
isDragging,
handleListeners,
resetHeight,
}
}