fix kanban drag drop marker

This commit is contained in:
callumalpass 2026-06-06 06:00:06 +10:00
parent 2ead7d59a0
commit 7c9a9ce015
5 changed files with 119 additions and 0 deletions

View file

@ -33,3 +33,5 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
-->
## Fixed
- (#1996) Made Kanban card drag-and-drop show a clear drop slot marker while cards are being repositioned, so the landing position remains visible during sorting. Thanks to @phillipadsmith for reporting this.

View file

@ -32,6 +32,7 @@ import { clearStaticStyleClasses } from "../utils/staticStyleClasses";
import { setElementDragImage } from "../utils/dragImage";
import {
applyKanbanTaskDropFrontmatterPlan,
clearKanbanDropMarkers,
createKanbanDropTarget,
getKanbanCardDropTargetFromClientY,
getKanbanDraggedPaths,
@ -43,6 +44,7 @@ import {
reconstructKanbanDropTargetFromContainer,
resolveKanbanContainerDropTarget,
resolveNestedTaskCardDragSource,
updateKanbanDropMarker,
type KanbanDropTarget,
type KanbanTaskDropUpdatePlan,
type KanbanTaskDragSource,
@ -2796,6 +2798,7 @@ export class KanbanView extends BasesViewBase {
i >= insertionIndex
);
}
updateKanbanDropMarker(container, siblings, insertionIndex);
}
});
}
@ -3167,6 +3170,7 @@ export class KanbanView extends BasesViewBase {
"tn-static-overflow-y-auto-03df744e",
"tn-static-overflow-y-clip-c5043043"
);
clearKanbanDropMarkers(this.dragContainer);
this.dragContainer.style.removeProperty("overflow-y");
this.dragContainer.style.removeProperty("padding-bottom");
const wrappers = this.dragContainer.querySelectorAll<HTMLElement>(
@ -3192,6 +3196,9 @@ export class KanbanView extends BasesViewBase {
}
// Also clean any wrappers on the entire board (safety net for cross-column)
if (this.boardEl) {
clearKanbanDropMarkers(this.boardEl);
}
this.boardEl
?.querySelectorAll<HTMLElement>(
".kanban-view__card-wrapper--drag-shift, .kanban-view__card-wrapper--shift-down"

View file

@ -224,6 +224,47 @@ export function reconstructKanbanDropTargetFromContainer({
return { taskPath: visibleCards[idx - 1].dataset.taskPath as string, above: false };
}
export function updateKanbanDropMarker(
cardsContainer: HTMLElement,
visibleCards: readonly HTMLElement[],
insertionIndex: number
): void {
cardsContainer.classList.remove("kanban-view__cards--drop-marker-end");
for (const card of visibleCards) {
card.classList.remove("kanban-view__card-wrapper--drop-marker");
}
const markerCard = visibleCards[insertionIndex];
if (markerCard) {
markerCard.classList.add("kanban-view__card-wrapper--drop-marker");
return;
}
if (visibleCards.length > 0 && insertionIndex >= visibleCards.length) {
cardsContainer.classList.add("kanban-view__cards--drop-marker-end");
}
}
export function clearKanbanDropMarkers(root: ParentNode): void {
if ((root as Node).nodeType === Node.ELEMENT_NODE) {
(root as HTMLElement).classList.remove(
"kanban-view__card-wrapper--drop-marker",
"kanban-view__cards--drop-marker-end"
);
}
root
.querySelectorAll<HTMLElement>(".kanban-view__card-wrapper--drop-marker")
.forEach((card) => {
card.classList.remove("kanban-view__card-wrapper--drop-marker");
});
root
.querySelectorAll<HTMLElement>(".kanban-view__cards--drop-marker-end")
.forEach((container) => {
container.classList.remove("kanban-view__cards--drop-marker-end");
});
}
export function performKanbanOptimisticReorder({
draggedPaths,
dropTarget,

View file

@ -697,6 +697,32 @@ body.is-mobile .tasknotes-plugin .kanban-view__swimlane-column {
transform: translateY(var(--tn-drag-gap, 60px));
}
.tasknotes-plugin .kanban-view__card-wrapper--drop-marker::before {
content: "";
position: absolute;
inset-inline: 0;
top: calc(-1 * var(--tn-drag-gap, 60px) + var(--tn-spacing-xs));
height: max(32px, calc(var(--tn-drag-gap, 60px) - (var(--tn-spacing-xs) * 2)));
box-sizing: border-box;
border: 2px dashed var(--tn-interactive-accent);
border-radius: var(--tn-radius-md);
background: color-mix(in srgb, var(--tn-interactive-accent) 12%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tn-interactive-accent) 20%, transparent);
pointer-events: none;
}
.tasknotes-plugin .kanban-view__cards--drop-marker-end::after,
.tasknotes-plugin .kanban-view__tasks-container.kanban-view__cards--drop-marker-end::after {
content: "";
display: block;
height: max(32px, calc(var(--tn-drag-gap, 60px) - (var(--tn-spacing-xs) * 2)));
box-sizing: border-box;
border: 2px dashed var(--tn-interactive-accent);
border-radius: var(--tn-radius-md);
background: color-mix(in srgb, var(--tn-interactive-accent) 12%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tn-interactive-accent) 20%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.tasknotes-plugin .kanban-view__card-wrapper--drag-shift {
transition: none;

View file

@ -1,5 +1,6 @@
import {
applyKanbanTaskDropFrontmatterPlan,
clearKanbanDropMarkers,
getKanbanCardDropTargetFromClientY,
getKanbanDraggedPaths,
kanbanDropPlanNeedsWrite,
@ -9,6 +10,7 @@ import {
planKanbanTaskDropUpdate,
reconstructKanbanDropTargetFromContainer,
resolveKanbanContainerDropTarget,
updateKanbanDropMarker,
type KanbanDropTarget,
} from "../../../src/bases/kanbanDragUtils";
import type { TaskInfo } from "../../../src/types";
@ -166,6 +168,47 @@ describe("kanbanDragUtils", () => {
).toEqual({ taskPath: "Tasks/c.md", above: false });
});
it("marks the card where the active drop gap is shown", () => {
const cards = document.createElement("div");
const first = createCard("Tasks/a.md");
const second = createCard("Tasks/b.md");
const third = createCard("Tasks/c.md");
cards.append(first, second, third);
updateKanbanDropMarker(cards, [first, second, third], 1);
expect(first.classList.contains("kanban-view__card-wrapper--drop-marker")).toBe(false);
expect(second.classList.contains("kanban-view__card-wrapper--drop-marker")).toBe(true);
expect(third.classList.contains("kanban-view__card-wrapper--drop-marker")).toBe(false);
expect(cards.classList.contains("kanban-view__cards--drop-marker-end")).toBe(false);
});
it("marks the end of a card container for append drops", () => {
const cards = document.createElement("div");
const first = createCard("Tasks/a.md");
const second = createCard("Tasks/b.md");
cards.append(first, second);
updateKanbanDropMarker(cards, [first, second], 2);
expect(first.classList.contains("kanban-view__card-wrapper--drop-marker")).toBe(false);
expect(second.classList.contains("kanban-view__card-wrapper--drop-marker")).toBe(false);
expect(cards.classList.contains("kanban-view__cards--drop-marker-end")).toBe(true);
});
it("clears Kanban drop marker classes from a drag root", () => {
const cards = document.createElement("div");
const first = createCard("Tasks/a.md");
cards.classList.add("kanban-view__cards--drop-marker-end");
first.classList.add("kanban-view__card-wrapper--drop-marker");
cards.appendChild(first);
clearKanbanDropMarkers(cards);
expect(first.classList.contains("kanban-view__card-wrapper--drop-marker")).toBe(false);
expect(cards.classList.contains("kanban-view__cards--drop-marker-end")).toBe(false);
});
it("appends dragged cards into an empty target container for optimistic cross-column drops", () => {
const source = document.createElement("div");
const target = document.createElement("div");