Remove drag and drop entirely

This commit is contained in:
2026-02-20 15:45:31 -08:00
parent a00b96231c
commit 1cbc127769
7 changed files with 227 additions and 281 deletions

View File

@@ -1,7 +1,7 @@
import { ItemView, Keymap, Platform, TFile, WorkspaceLeaf } from 'obsidian';
import type { TodoItem, TodoGroup } from '../core/types';
import { parseTodosGroupedByHeading } from '../core/todo-parser';
import { toggleTodo, moveTodoWithChildren, moveTodoUnderHeadingInFile, indentTodoLines } from '../core/todo-transformer';
import { toggleTodo, collectTodoBlockLines, removeTodoBlock } from '../core/todo-transformer';
import { createTodoItemEl, collectChildLineNumbers } from './todo-item-component';
export const TODO_VIEW_TYPE = 'todo-tracker-view';
@@ -17,8 +17,6 @@ export class TodoSidebarView extends ItemView {
private flatTodoList: FlatTodoEntry[] = [];
private focusedIndex = -1;
private groups: TodoGroup[] = [];
private draggedTodo: TodoItem | null = null;
private draggedChildLines: number[] = [];
constructor(leaf: WorkspaceLeaf) {
super(leaf);
@@ -127,9 +125,6 @@ export class TodoSidebarView extends ItemView {
for (const todo of group.todos) {
this.renderTodoItem(listEl, todo, file, group);
}
// Set up drop zone on the group
this.setupGroupDropZone(groupEl, group, file);
}
// Don't auto-focus — user activates via ArrowDown (like Outline view)
@@ -150,16 +145,11 @@ export class TodoSidebarView extends ItemView {
onToggle: (t) => this.handleToggle(file, t),
onMoveClick: (t) => this.handleMoveClick(t, file),
onClick: (t) => this.openTodoInEditor(file, t),
onDragStart: (evt, t) => this.handleDragStart(evt, t),
onDragEnd: (evt) => this.handleDragEnd(evt),
});
// Register in flat list for keyboard navigation
this.flatTodoList.push({ todo, element: itemEl, group });
// Set up drop target on individual items
this.setupItemDropZone(itemEl, todo, file);
// Recursively register children in flat list
for (const child of todo.children) {
this.registerChildInFlatList(itemEl, child, group);
@@ -177,7 +167,6 @@ export class TodoSidebarView extends ItemView {
) as HTMLElement | null;
if (childEl) {
this.flatTodoList.push({ todo, element: childEl, group });
this.setupItemDropZone(childEl, todo, this.currentFile!);
for (const grandchild of todo.children) {
this.registerChildInFlatList(childEl, grandchild, group);
@@ -185,219 +174,6 @@ export class TodoSidebarView extends ItemView {
}
}
// --- Drag and Drop ---
private handleDragStart(evt: DragEvent, todo: TodoItem): void {
this.draggedTodo = todo;
this.draggedChildLines = collectChildLineNumbers(todo);
evt.dataTransfer?.setData('text/plain', todo.text);
if (evt.dataTransfer) {
evt.dataTransfer.effectAllowed = 'move';
}
// Add dragging class
const target = evt.target as HTMLElement;
target.addClass('todo-tracker-item-dragging');
}
private handleDragEnd(evt: DragEvent): void {
this.draggedTodo = null;
this.draggedChildLines = [];
// Remove dragging class
const target = evt.target as HTMLElement;
target.removeClass('todo-tracker-item-dragging');
// Clean up all drag-over indicators
this.contentEl.querySelectorAll('.todo-tracker-drag-over').forEach((el) => {
el.classList.remove('todo-tracker-drag-over');
});
this.contentEl.querySelectorAll('.todo-tracker-drop-above, .todo-tracker-drop-below, .todo-tracker-drop-nest').forEach((el) => {
el.classList.remove('todo-tracker-drop-above');
el.classList.remove('todo-tracker-drop-below');
el.classList.remove('todo-tracker-drop-nest');
});
}
private getDropZone(evt: DragEvent, itemEl: HTMLElement): 'above' | 'nest' | 'below' {
const rect = itemEl.getBoundingClientRect();
const relativeY = evt.clientY - rect.top;
const thirdHeight = rect.height / 3;
if (relativeY < thirdHeight) return 'above';
if (relativeY < thirdHeight * 2) return 'nest';
return 'below';
}
private setupItemDropZone(itemEl: HTMLElement, targetTodo: TodoItem, file: TFile): void {
itemEl.addEventListener('dragover', (evt) => {
if (!this.draggedTodo || this.draggedTodo.lineNumber === targetTodo.lineNumber) return;
evt.preventDefault();
if (evt.dataTransfer) {
evt.dataTransfer.dropEffect = 'move';
}
const zone = this.getDropZone(evt, itemEl);
itemEl.removeClass('todo-tracker-drop-above');
itemEl.removeClass('todo-tracker-drop-below');
itemEl.removeClass('todo-tracker-drop-nest');
if (zone === 'above') itemEl.addClass('todo-tracker-drop-above');
else if (zone === 'below') itemEl.addClass('todo-tracker-drop-below');
else itemEl.addClass('todo-tracker-drop-nest');
});
itemEl.addEventListener('dragleave', () => {
itemEl.removeClass('todo-tracker-drop-above');
itemEl.removeClass('todo-tracker-drop-below');
itemEl.removeClass('todo-tracker-drop-nest');
});
itemEl.addEventListener('drop', (evt) => {
evt.preventDefault();
itemEl.removeClass('todo-tracker-drop-above');
itemEl.removeClass('todo-tracker-drop-below');
itemEl.removeClass('todo-tracker-drop-nest');
if (!this.draggedTodo || !file) return;
const zone = this.getDropZone(evt, itemEl);
if (zone === 'nest') {
this.performNest(file, targetTodo);
} else {
const targetLine = zone === 'above' ? targetTodo.lineNumber : targetTodo.lineNumber + 1;
this.performMove(file, targetLine);
}
});
}
private setupGroupDropZone(groupEl: HTMLElement, group: TodoGroup, file: TFile): void {
groupEl.addEventListener('dragover', (evt) => {
if (!this.draggedTodo) return;
// Only handle if drop is on the group itself, not a child item
const target = evt.target as HTMLElement;
if (target.closest('.todo-tracker-item')) return;
evt.preventDefault();
if (evt.dataTransfer) {
evt.dataTransfer.dropEffect = 'move';
}
groupEl.addClass('todo-tracker-drag-over');
});
groupEl.addEventListener('dragleave', (evt) => {
// Only remove if actually leaving the group
const relatedTarget = evt.relatedTarget as HTMLElement | null;
if (!relatedTarget || !groupEl.contains(relatedTarget)) {
groupEl.removeClass('todo-tracker-drag-over');
}
});
groupEl.addEventListener('drop', (evt) => {
evt.preventDefault();
groupEl.removeClass('todo-tracker-drag-over');
if (!this.draggedTodo || !file || !group.heading) return;
// Find the next heading's line number for section boundary
const groupIndex = this.groups.indexOf(group);
let nextHeadingLine: number | undefined;
for (let i = groupIndex + 1; i < this.groups.length; i++) {
if (this.groups[i]?.heading) {
nextHeadingLine = this.groups[i]!.heading!.lineNumber;
break;
}
}
this.performMoveUnderHeading(file, group.heading.lineNumber, nextHeadingLine);
});
}
private async performMove(file: TFile, targetLine: number): Promise<void> {
if (!this.draggedTodo) return;
const todoLine = this.draggedTodo.lineNumber;
const childLines = this.draggedChildLines;
await this.app.vault.process(file, (content) => {
return moveTodoWithChildren(content, todoLine, childLines, targetLine);
});
this.draggedTodo = null;
this.draggedChildLines = [];
}
private async performNest(file: TFile, targetTodo: TodoItem): Promise<void> {
if (!this.draggedTodo) return;
const todoLine = this.draggedTodo.lineNumber;
const childLines = this.draggedChildLines;
const targetIndent = targetTodo.indentLevel;
await this.app.vault.process(file, (content) => {
const lines = content.split('\n');
const allLineNumbers = [todoLine, ...childLines].sort((a, b) => a - b);
// Extract lines to move
const movedLines = allLineNumbers.map((ln) => lines[ln]!);
// Calculate indent delta: we want dragged todo to be one level deeper than target
const draggedIndent = this.draggedTodo!.indentLevel;
const indentDelta = targetIndent + 1 - draggedIndent;
// Adjust indentation
const indentedLines = indentTodoLines(movedLines, indentDelta);
// Find insert position: right after target todo and its existing children
const targetChildLines = collectChildLineNumbers(targetTodo);
const allTargetLines = [targetTodo.lineNumber, ...targetChildLines];
const insertAfter = Math.max(...allTargetLines);
// Remove original lines (in reverse to preserve indices)
for (let i = allLineNumbers.length - 1; i >= 0; i--) {
lines.splice(allLineNumbers[i]!, 1);
}
// Adjust insert position for removed lines
let adjustedInsert = insertAfter;
for (const ln of allLineNumbers) {
if (ln <= insertAfter) {
adjustedInsert--;
}
}
// Insert indented lines after the target
lines.splice(adjustedInsert + 1, 0, ...indentedLines);
return lines.join('\n');
});
this.draggedTodo = null;
this.draggedChildLines = [];
}
private async performMoveUnderHeading(
file: TFile,
headingLine: number,
nextHeadingLine?: number
): Promise<void> {
if (!this.draggedTodo) return;
const todoLine = this.draggedTodo.lineNumber;
const childLines = this.draggedChildLines;
await this.app.vault.process(file, (content) => {
return moveTodoUnderHeadingInFile(content, todoLine, childLines, headingLine, nextHeadingLine);
});
this.draggedTodo = null;
this.draggedChildLines = [];
}
// --- Keyboard Navigation ---
private handleKeydown(evt: KeyboardEvent): void {