// HTML5 drag-and-drop helpers, no libraries. Two flows: // • Calendar — drag a day chip onto another in-range day to move that entry // (shifts end_date by the same delta so multi-day spans stay intact). // • Day editor — drag a row's handle to reorder entries within one day. // Touch devices don't fire HTML5 DnD events, so both flows are desktop-only and // degrade to the existing click/edit behaviour with no polyfill. import { api } from '../api.js'; import { toast } from '../dom.js'; import { parseYMD, ymd, addDays } from '../format.js'; const DND_MIME = 'text/plain'; function dayDelta(fromYmd, toYmd) { return Math.round((parseYMD(toYmd) - parseYMD(fromYmd)) / 86400000); } // ---------- Calendar: move an entry to another day ---------- // Mark a calendar chip as a drag source carrying its entry id. export function makeChipDraggable(node, entry) { node.setAttribute('draggable', 'true'); node.classList.add('cal-chip-draggable'); node.addEventListener('dragstart', (e) => { e.dataTransfer.setData(DND_MIME, String(entry.id)); e.dataTransfer.effectAllowed = 'move'; node.classList.add('dragging'); }); node.addEventListener('dragend', () => node.classList.remove('dragging')); } // Wire an in-range day cell as a drop target. On drop the dragged entry moves to // `targetDate` with sort_order = `targetCount` (appended after that day's // entries); an entry with an end_date has it shifted by the same day-delta in // the SAME patch (the backend rejects date > stored end_date otherwise). export function makeDayDropTarget(cell, targetDate, targetCount, entriesById, tctx) { cell.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; cell.classList.add('cal-day-drop'); }); cell.addEventListener('dragleave', (e) => { if (!cell.contains(e.relatedTarget)) cell.classList.remove('cal-day-drop'); }); cell.addEventListener('drop', async (e) => { e.preventDefault(); cell.classList.remove('cal-day-drop'); const id = Number(e.dataTransfer.getData(DND_MIME)); if (!id) return; const entry = entriesById.get(id); if (!entry || entry.date === targetDate) return; // dropped on its own day const patch = { date: targetDate, sort_order: targetCount }; if (entry.end_date) { const delta = dayDelta(entry.date, targetDate); patch.end_date = ymd(addDays(parseYMD(entry.end_date), delta)); } try { await api.entries.update(id, patch); await tctx.refreshTrip(); } catch (err) { toast(err.message); } }); } // ---------- Day editor: reorder entries within a day ---------- // `rows`: [{ node, handle, entry }] in current display order. Dragging a row's // handle over another row shows a top/bottom insertion indicator; on drop the // new order is computed and sort_order PATCHed for ONLY the entries whose index // changed, then `refresh()` re-renders (the editor stays open on the same day). export function enableRowReorder(rows, refresh) { let draggingId = null; const clearAll = () => { for (const r of rows) r.node.classList.remove('drop-before', 'drop-after', 'dragging'); }; for (const { node, handle, entry } of rows) { handle.setAttribute('draggable', 'true'); handle.addEventListener('dragstart', (e) => { draggingId = entry.id; e.dataTransfer.setData(DND_MIME, String(entry.id)); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setDragImage(node, 0, 0); node.classList.add('dragging'); }); handle.addEventListener('dragend', () => { draggingId = null; clearAll(); }); node.addEventListener('dragover', (e) => { if (draggingId == null || entry.id === draggingId) return; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const rect = node.getBoundingClientRect(); const after = e.clientY - rect.top > rect.height / 2; node.classList.toggle('drop-after', after); node.classList.toggle('drop-before', !after); }); node.addEventListener('dragleave', (e) => { if (!node.contains(e.relatedTarget)) node.classList.remove('drop-before', 'drop-after'); }); node.addEventListener('drop', async (e) => { if (draggingId == null || entry.id === draggingId) return; e.preventDefault(); const after = node.classList.contains('drop-after'); const movedId = draggingId; clearAll(); draggingId = null; await commitReorder(rows, movedId, entry.id, after, refresh); }); } } async function commitReorder(rows, movedId, targetId, after, refresh) { const moved = rows.find((r) => r.entry.id === movedId).entry; const ordered = rows.map((r) => r.entry).filter((e) => e.id !== movedId); const targetIndex = ordered.findIndex((e) => e.id === targetId); ordered.splice(after ? targetIndex + 1 : targetIndex, 0, moved); try { for (let i = 0; i < ordered.length; i++) { if (ordered[i].sort_order !== i) { await api.entries.update(ordered[i].id, { sort_order: i }); } } await refresh(); } catch (err) { toast(err.message); } }