diff --git a/frontend/src/components/Icon.vue b/frontend/src/components/Icon.vue index 19faf9e..1271f78 100644 --- a/frontend/src/components/Icon.vue +++ b/frontend/src/components/Icon.vue @@ -18,6 +18,7 @@ const paths: Record = { image: '', graph: '', bell: '', + grip: '', }; diff --git a/frontend/src/components/NoteCard.vue b/frontend/src/components/NoteCard.vue index 49ff9a2..45ffd1d 100644 --- a/frontend/src/components/NoteCard.vue +++ b/frontend/src/components/NoteCard.vue @@ -19,12 +19,52 @@ const props = defineProps<{ note: Note; reorderable?: boolean; active?: boolean const emit = defineEmits<{ (e: "open", note: Note): void; (e: "dragstart", note: Note): void; + (e: "dragend", note: Note): void; (e: "drop", note: Note): void; }>(); const notes = useNotesStore(); -// When this card becomes the keyboard-focused card, scroll it into view. const root = ref(null); + +// --- Drag-to-reorder. Native HTML5 DnD, gated behind an explicit grip handle so +// a plain click/select never starts a drag by accident. `dragging` dims the +// source card; `dragOver` shows where the drop will land. --- +const canDrag = () => !!props.reorderable && !props.note.trashed; +const grabbing = ref(false); // handle pressed → the card is momentarily draggable +const dragging = ref(false); // this card is the one being dragged +const dragOver = ref(false); // another card is hovering over this one as a drop target + +function onDragStart(e: DragEvent) { + dragging.value = true; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", props.note.id); // some browsers need a payload to drag + } + emit("dragstart", props.note); +} +function onDragEnd() { + dragging.value = false; + grabbing.value = false; + dragOver.value = false; + emit("dragend", props.note); +} +function onDragOver(e: DragEvent) { + e.preventDefault(); // allow drop + if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; + if (!dragging.value) dragOver.value = true; // don't flag the source as its own target +} +function onDragLeave(e: DragEvent) { + // dragleave also fires when moving onto a child; only clear when truly leaving the card. + const to = e.relatedTarget as Node | null; + if (to && root.value?.contains(to)) return; + dragOver.value = false; +} +function onDrop() { + dragOver.value = false; + emit("drop", props.note); +} + +// When this card becomes the keyboard-focused card, scroll it into view. watch( () => props.active, (a) => { @@ -67,12 +107,37 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
+ + + (null); function onDragStart(note: Note) { draggingId.value = note.id; } +function onDragEnd() { + // Clear the tracked source even when a drag is cancelled (dropped on nothing). + draggingId.value = null; +} async function onDrop(target: Note) { const from = draggingId.value; draggingId.value = null; @@ -205,6 +209,7 @@ async function onDrop(target: Note) { reorderable @open="openEditor" @dragstart="onDragStart" + @dragend="onDragEnd" @drop="onDrop" />
@@ -222,6 +227,7 @@ async function onDrop(target: Note) { reorderable @open="openEditor" @dragstart="onDragStart" + @dragend="onDragEnd" @drop="onDrop" /> @@ -237,6 +243,7 @@ async function onDrop(target: Note) { reorderable @open="openEditor" @dragstart="onDragStart" + @dragend="onDragEnd" @drop="onDrop" />