mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-25 00:13:00 +00:00
111 lines
4.4 KiB
TypeScript
111 lines
4.4 KiB
TypeScript
import type { RoadtripDayBoundary } from '@trek/shared'
|
|
import type { RouteVia } from '../../types'
|
|
import { nearestDayBoundary, type DayBoundaryLeg, type DayBoundaryTarget, type ProjectPoint } from '../Roadtrip/dayBoundaryPath'
|
|
|
|
export interface DayBoundaryControls {
|
|
path: DayBoundaryLeg[]
|
|
move: (day: number, boundary: RoadtripDayBoundary | null) => Promise<boolean>
|
|
hint: string
|
|
}
|
|
|
|
interface DragMap {
|
|
project: ProjectPoint
|
|
setPosition: (lat: number, lng: number) => void
|
|
lock: () => () => void
|
|
}
|
|
|
|
export function bindDayBoundaryDrag(el: HTMLElement, via: RouteVia, controls: DayBoundaryControls, map: DragMap) {
|
|
const pause = via.nightPause!
|
|
// Leaflet replaces the label contents when its icon changes, but keeps this root.
|
|
const handle = el
|
|
handle.style.cursor = 'grab'
|
|
handle.style.touchAction = 'none'
|
|
handle.tabIndex = 0
|
|
handle.setAttribute('role', 'button')
|
|
handle.setAttribute('aria-label', via.label ?? '')
|
|
handle.setAttribute('aria-description', controls.hint)
|
|
let origin: { x: number; y: number } | null = null
|
|
let target: DayBoundaryTarget | null = null
|
|
let unlock: (() => void) | null = null
|
|
let moved = false
|
|
let saving = false
|
|
let disposed = false
|
|
const restore = () => map.setPosition(via.lat, via.lng)
|
|
const detach = () => {
|
|
document.removeEventListener('pointermove', move)
|
|
document.removeEventListener('pointerup', up)
|
|
document.removeEventListener('pointercancel', cancel)
|
|
document.removeEventListener('keydown', escape)
|
|
unlock?.()
|
|
unlock = null
|
|
origin = null
|
|
handle.style.cursor = 'grab'
|
|
}
|
|
const cancel = () => { detach(); restore() }
|
|
const escape = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault(); cancel() } }
|
|
const commit = async (boundary: RoadtripDayBoundary | null) => {
|
|
saving = true
|
|
handle.setAttribute('aria-busy', 'true')
|
|
try { if (!await controls.move(pause.day, boundary) && !disposed) restore() }
|
|
finally { saving = false; handle.removeAttribute('aria-busy') }
|
|
}
|
|
const move = (event: PointerEvent) => {
|
|
if (!origin) return
|
|
if (!moved && Math.hypot(event.clientX - origin.x, event.clientY - origin.y) < 4) return
|
|
moved = true
|
|
event.preventDefault()
|
|
target = nearestDayBoundary(controls.path, pause.day, { x: event.clientX, y: event.clientY }, map.project, pause.minPosition, pause.maxPosition)
|
|
if (target) map.setPosition(target.lat, target.lng)
|
|
}
|
|
const up = () => {
|
|
const next = moved ? target : null
|
|
detach()
|
|
if (next) void commit(next.boundary)
|
|
else restore()
|
|
}
|
|
const down = (event: PointerEvent) => {
|
|
if (event.button !== 0 || saving) return
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
origin = { x: event.clientX, y: event.clientY }
|
|
target = null
|
|
moved = false
|
|
unlock = map.lock()
|
|
handle.style.cursor = 'grabbing'
|
|
document.addEventListener('pointermove', move, { passive: false })
|
|
document.addEventListener('pointerup', up)
|
|
document.addEventListener('pointercancel', cancel)
|
|
document.addEventListener('keydown', escape)
|
|
}
|
|
const click = (event: Event) => {
|
|
if (moved || saving) { event.preventDefault(); event.stopImmediatePropagation() }
|
|
}
|
|
const reset = (event: Event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
if (!saving && pause.manual) void commit(null)
|
|
}
|
|
const key = (event: KeyboardEvent) => {
|
|
if (event.key === 'Delete' || event.key === 'Backspace') return reset(event)
|
|
if (!['ArrowLeft', 'ArrowRight'].includes(event.key) || saving) return
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
const position = (pause.position ?? 0) + (event.key === 'ArrowLeft' ? -0.05 : 0.05)
|
|
const leg = controls.path.find(leg => position >= leg.position && position <= leg.position + 1)
|
|
if (!leg || position <= (pause.minPosition ?? -1) || position >= (pause.maxPosition ?? Infinity)) return
|
|
void commit({ day_number: pause.day, from_assignment_id: leg.from.assignmentId, to_assignment_id: leg.to.assignmentId, fraction: position - leg.position })
|
|
}
|
|
handle.addEventListener('pointerdown', down)
|
|
el.addEventListener('click', click, true)
|
|
handle.addEventListener('contextmenu', reset)
|
|
handle.addEventListener('keydown', key)
|
|
return () => {
|
|
disposed = true
|
|
detach()
|
|
handle.removeEventListener('pointerdown', down)
|
|
el.removeEventListener('click', click, true)
|
|
handle.removeEventListener('contextmenu', reset)
|
|
handle.removeEventListener('keydown', key)
|
|
}
|
|
}
|