137 lines
3.3 KiB
JavaScript
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,
|
|
}
|
|
}
|