mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
57ea219a43
This commit completes the reviewed Tours follow-up scope after the Tours MVP was merged into dev. Included feature blocks: - RC-01: identify Tours in PLAN day rows and show compact Tour facts; - RC-06: search for a city or address in TOUR-PLANNER without hidden writes; - RC-07: reorder Tour waypoints in the left sidebar; - RC-08: drag Tour waypoints on Leaflet, MapLibre and Mapbox with stable viewport, history and routing behavior; - RS-01: persist Tour descriptions and HTTPS information links with local provider classification; - RS-02: persist breaks/additional time and calculated or manually adjusted planned Tour duration without changing scheduling semantics. The implementation preserves: - ordinary Place and Legacy Track behavior; - Tours-off fallback behavior; - existing GPX/import behavior; - Tour assignment and deletion behavior; - calculated routing duration as a separate value; - existing Tour provenance fields; - keyboard/list alternatives for waypoint editing. Not included: - PR1-09 trip-wide Tour summary; - Wanderer integration; - GPX import changes; - new Tour types; - Tour-to-Day drag-and-drop; - further roadmap or Post-Release features. Validation: - focused RC-01/RC-06/RC-07/RC-08/RS-01/RS-02 tests passed; - affected shared/server/client typechecks passed; - migration and generated DB type checks passed; - scoped ESLint passed; - staged diff checks passed; - owner manual tests passed for all six feature blocks.
125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
export 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;
|
|
};
|
|
}
|