Files
boursecu_user/src/utils/useDraggableFloatingButton.ts
T

166 lines
3.7 KiB
TypeScript
Raw Normal View History

2026-06-13 13:52:47 +03:30
import { computed, onMounted, onUnmounted, ref } from 'vue'
const DRAG_THRESHOLD = 6
type FabPosition = { x: number; y: number }
type DraggableFloatingOptions = {
width?: number
height?: number
defaultBottomOffset?: number
defaultRightOffset?: number
}
export function useDraggableFloatingButton(
storageKey: string,
options: DraggableFloatingOptions = {}
) {
const width = options.width ?? 96
const height = options.height ?? 44
const defaultBottomOffset = options.defaultBottomOffset ?? 120
const defaultRightOffset = options.defaultRightOffset ?? 20
const pos = ref<FabPosition>({ x: 0, y: 0 })
const dragging = ref(false)
let pointerId: number | null = null
let startX = 0
let startY = 0
let originX = 0
let originY = 0
let hasMoved = false
const fabStyle = computed(() => ({
left: `${pos.value.x}px`,
top: `${pos.value.y}px`,
width: `${width}px`,
touchAction: 'none'
}))
function getViewportWidth() {
return Math.min(window.innerWidth, 426)
}
function getHorizontalOffset() {
const vw = window.innerWidth
const contentWidth = getViewportWidth()
return Math.max(0, (vw - contentWidth) / 2)
}
function resetToDefault() {
const vh = window.innerHeight
const offsetX = getHorizontalOffset()
const contentWidth = getViewportWidth()
pos.value = {
x: offsetX + contentWidth - width - defaultRightOffset,
y: vh - defaultBottomOffset - height
}
}
function clampToViewport() {
const vw = window.innerWidth
const vh = window.innerHeight
pos.value = {
x: Math.max(0, Math.min(vw - width, pos.value.x)),
y: Math.max(0, Math.min(vh - height, pos.value.y))
}
}
function loadPosition() {
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return false
const parsed = JSON.parse(raw) as FabPosition
if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {
pos.value = parsed
clampToViewport()
return true
}
} catch {
// ignore
}
return false
}
function savePosition() {
localStorage.setItem(storageKey, JSON.stringify(pos.value))
}
function handleResize() {
clampToViewport()
savePosition()
}
function handlePointerDown(event: PointerEvent) {
dragging.value = true
hasMoved = false
pointerId = event.pointerId
startX = event.clientX
startY = event.clientY
originX = pos.value.x
originY = pos.value.y
;(event.currentTarget as HTMLElement)?.setPointerCapture?.(event.pointerId)
}
function handlePointerMove(event: PointerEvent) {
if (!dragging.value || pointerId !== event.pointerId) return
const dx = event.clientX - startX
const dy = event.clientY - startY
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
hasMoved = true
}
pos.value = {
x: originX + dx,
y: originY + dy
}
clampToViewport()
}
function finishPointer(event: PointerEvent) {
if (!dragging.value || pointerId !== event.pointerId) return false
dragging.value = false
pointerId = null
savePosition()
const clicked = !hasMoved
hasMoved = false
return clicked
}
function handlePointerUp(event: PointerEvent) {
return finishPointer(event)
}
function handlePointerCancel(event: PointerEvent) {
finishPointer(event)
return false
}
onMounted(() => {
if (!loadPosition()) {
resetToDefault()
}
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
return {
dragging,
fabStyle,
handlePointerDown,
handlePointerMove,
handlePointerUp,
handlePointerCancel,
resetToDefault
}
}