Files
TREK/client/src/components/Map/plannerWaypointDrag.ts
T

125 lines
4.3 KiB
TypeScript

interface PlannerWaypointCoordinate {
lat: number;
lng: number;
}
interface PlannerWaypointDragOptions {
element: HTMLElement;
initial: PlannerWaypointCoordinate;
coordinateAt: (clientX: number, clientY: number) => PlannerWaypointCoordinate;
setPosition: (coordinate: PlannerWaypointCoordinate) => void;
onCommit: (coordinate: PlannerWaypointCoordinate) => boolean | void;
}
const DRAG_THRESHOLD_PX = 4;
export function wirePlannerWaypointDrag({
element,
initial,
coordinateAt,
setPosition,
onCommit,
}: PlannerWaypointDragOptions): () => void {
let pointerId: number | null = null;
let start: { x: number; y: number } | null = null;
let dragged = false;
let suppressClick = false;
const originalTouchAction = element.style.touchAction;
const originalUserSelect = element.style.userSelect;
const stopMapGesture = (event: Event) => event.stopPropagation();
const restore = () => setPosition(initial);
const cancel = (event?: Event) => {
if (pointerId === null) return;
event?.stopPropagation();
restore();
pointerId = null;
start = null;
dragged = false;
suppressClick = false;
};
const onPointerDown = (event: PointerEvent) => {
if (event.button !== 0 || pointerId !== null) {
event.stopPropagation();
return;
}
event.stopPropagation();
pointerId = event.pointerId;
start = { x: event.clientX, y: event.clientY };
dragged = false;
suppressClick = false;
try {
element.setPointerCapture(event.pointerId);
} catch {
// Pointer capture is unavailable in some test DOMs and older browsers.
}
};
const onPointerMove = (event: PointerEvent) => {
if (event.pointerId !== pointerId || !start) return;
event.stopPropagation();
if (!dragged && Math.hypot(event.clientX - start.x, event.clientY - start.y) < DRAG_THRESHOLD_PX) return;
dragged = true;
event.preventDefault();
setPosition(coordinateAt(event.clientX, event.clientY));
};
const onPointerUp = (event: PointerEvent) => {
if (event.pointerId !== pointerId || !start) return;
event.stopPropagation();
const travelled = Math.hypot(event.clientX - start.x, event.clientY - start.y);
if (dragged || travelled >= DRAG_THRESHOLD_PX) {
const coordinate = coordinateAt(event.clientX, event.clientY);
setPosition(coordinate);
const accepted = onCommit(coordinate) !== false;
suppressClick = true;
if (!accepted) restore();
}
pointerId = null;
start = null;
dragged = false;
};
const onClickCapture = (event: MouseEvent) => {
if (!suppressClick) return;
suppressClick = false;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
};
const onTouchCancel = (event: Event) => cancel(event);
element.addEventListener('pointerdown', onPointerDown);
element.addEventListener('pointermove', onPointerMove);
element.addEventListener('pointerup', onPointerUp);
element.addEventListener('pointercancel', cancel);
element.addEventListener('lostpointercapture', cancel);
element.addEventListener('click', onClickCapture, true);
element.addEventListener('mousedown', stopMapGesture);
element.addEventListener('touchstart', stopMapGesture, { passive: true });
element.addEventListener('touchmove', stopMapGesture, { passive: true });
element.addEventListener('touchend', stopMapGesture, { passive: true });
element.addEventListener('touchcancel', onTouchCancel, { passive: true });
element.style.touchAction = 'none';
element.style.userSelect = 'none';
return () => {
cancel();
element.removeEventListener('pointerdown', onPointerDown);
element.removeEventListener('pointermove', onPointerMove);
element.removeEventListener('pointerup', onPointerUp);
element.removeEventListener('pointercancel', cancel);
element.removeEventListener('lostpointercapture', cancel);
element.removeEventListener('click', onClickCapture, true);
element.removeEventListener('mousedown', stopMapGesture);
element.removeEventListener('touchstart', stopMapGesture);
element.removeEventListener('touchmove', stopMapGesture);
element.removeEventListener('touchend', stopMapGesture);
element.removeEventListener('touchcancel', onTouchCancel);
element.style.touchAction = originalTouchAction;
element.style.userSelect = originalUserSelect;
};
}