Files
TREK/client/src/components/Map/markerDrag.ts
T
Maurice 782e613506 Tours: day GPX import, wanderer trails and planning over an import
- GPX files drop onto the map or a day; the import lands on that day
- wanderer connection per user in Integrations; the workspace offers the
  user's own trails only, behind an admin switch under the Tours addon
- wanderer difficulty is stored on the SAC scale (T1 to T3), backfilled
- a GPX or wanderer import can be planned as an editable tour, either in
  place (the recorded track is kept and can be restored) or as a new tour
  that remembers the import it came from
- rails, rows and detail dialog follow the plan sidebar's cards and badges
- admin addon shelves with more than one row fold, collapsed by default
- reverse-geocoded waypoint names are cached on the device
2026-10-15 17:44:15 +02:00

115 lines
4.8 KiB
TypeScript

/**
* Dragging a place off the map onto a day in the itinerary (#891).
*
* The drop half of this already existed: every day, every row and every gap
* between rows in the day plan reads a `placeId` and calls onAssignToDay with
* the position it was dropped at. All that was missing was a second place to
* pick a place UP from — until now the only one was the row in the places
* sidebar. This makes a map marker behave exactly like that row.
*
* `window.__dragData` is set alongside dataTransfer on purpose, and is not
* belt-and-braces: browsers hide dataTransfer's contents during `dragover`, so
* a drop target that wants to know what is coming while it hovers has no other
* way to look. The day plan reads both (getDragData), and this mirrors what
* PlacesSidebarRow already does so the two sources are indistinguishable to it.
*
* Both renderers get the same treatment through this one helper: Leaflet builds
* its markers from divIcon HTML and the GL renderers from createElement, but
* both end up as a real DOM node, which is all HTML5 drag needs.
*/
/**
* Start dragging `placeId` towards the day plan, from any row or marker that holds a
* place (a tour is one too). Every source goes through here, so the day plan cannot
* tell them apart.
*/
export function startPlaceDrag(transfer: DataTransfer | null, placeId: number): void {
transfer?.setData('placeId', String(placeId))
if (transfer) transfer.effectAllowed = 'copy'
window.__dragData = { placeId: String(placeId) }
}
/** The drag `startPlaceDrag` began has ended, dropped or not. */
export function endPlaceDrag(): void {
window.__dragData = null
}
/** Wire a marker element up as a drag source for `placeId`. */
export function makeMarkerDraggable(el: HTMLElement, placeId: number): () => void {
el.setAttribute('draggable', 'true')
const onDragStart = (e: DragEvent) => {
startPlaceDrag(e.dataTransfer, placeId)
el.classList.add('marker-dragging')
}
const onDragEnd = () => {
endPlaceDrag()
el.classList.remove('marker-dragging')
}
// The map pans on mousedown anywhere on its surface, and a marker sits on that
// surface. Without this the map slides away under the pointer while the drag
// is starting, which makes the day plan an impossible target to reach.
// The map then never sees the press that starts a click on the marker, which is why the Leaflet
// renderer clears the map's pan flag itself when a pin is pressed (wireDraggablePin, #2504).
const onMouseDown = (e: MouseEvent) => e.stopPropagation()
el.addEventListener('dragstart', onDragStart)
el.addEventListener('dragend', onDragEnd)
el.addEventListener('mousedown', onMouseDown)
return () => {
el.removeAttribute('draggable')
el.removeEventListener('dragstart', onDragStart)
el.removeEventListener('dragend', onDragEnd)
el.removeEventListener('mousedown', onMouseDown)
}
}
/**
* Wire a corridor hit up as a drag source, so it can be dropped onto the drive.
*
* The same HTML5 drag as `makeMarkerDraggable`, with the OSM id as payload instead of a
* place id — a corridor hit is not a place yet. The drop target is the map itself rather
* than the drawn line: a route polyline is a real element only in Leaflet (the GL
* renderers draw it into the canvas), and the drop coordinate answers "where on the drive"
* just as well once it is projected onto the routed geometry.
*/
export function makePoiDraggable(el: HTMLElement, osmId: string): () => void {
el.setAttribute('draggable', 'true')
const onDragStart = (e: DragEvent) => {
e.dataTransfer?.setData('poiOsmId', osmId)
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'copy'
window.__dragData = { poiOsmId: osmId }
el.classList.add('marker-dragging')
document.body.classList.add('dragging-poi')
}
const onDragEnd = () => {
window.__dragData = null
el.classList.remove('marker-dragging')
document.body.classList.remove('dragging-poi')
}
// Same reason as above: without this the map pans out from under the pointer as the
// drag begins, and the route is impossible to aim at.
const onMouseDown = (e: MouseEvent) => e.stopPropagation()
el.addEventListener('dragstart', onDragStart)
el.addEventListener('dragend', onDragEnd)
el.addEventListener('mousedown', onMouseDown)
return () => {
el.removeAttribute('draggable')
el.removeEventListener('dragstart', onDragStart)
el.removeEventListener('dragend', onDragEnd)
el.removeEventListener('mousedown', onMouseDown)
}
}
/** The OSM id being dragged, when a corridor hit is in flight. */
export function draggedPoiId(e: DragEvent): string | null {
const fromTransfer = e.dataTransfer?.getData('poiOsmId')
if (fromTransfer) return fromTransfer
// dataTransfer is unreadable during dragover, so the window mirror answers instead.
return window.__dragData?.poiOsmId ?? null
}