Add heading-grouped, nested, draggable todos with keyboard navigation
Enhance the todo tracker sidebar panel with: - Group todos by heading from the source file - Display nested (indented) todos as a tree - Drag-and-drop to reorder todos within/across heading groups - Keyboard navigation (arrow keys, Enter to open, M to move, toggle hotkey) - Right-click context menu replacing the ellipsis button - Proper focus management (no focus stealing on refresh) - Platform-aware Obsidian hotkey matching for toggle checkbox Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,28 @@
|
||||
import { Menu, setIcon } from 'obsidian';
|
||||
import { Menu } from 'obsidian';
|
||||
import type { TodoItem } from '../core/types';
|
||||
|
||||
export interface TodoItemCallbacks {
|
||||
onToggle: () => void;
|
||||
onMoveClick: () => void;
|
||||
onToggle: (todo: TodoItem) => void;
|
||||
onMoveClick: (todo: TodoItem) => void;
|
||||
onDragStart: (evt: DragEvent, todo: TodoItem) => void;
|
||||
onDragEnd: (evt: DragEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DOM element for a todo item with checkbox and menu.
|
||||
* Collect all line numbers for a todo and its descendants (for drag data).
|
||||
*/
|
||||
export function collectChildLineNumbers(todo: TodoItem): number[] {
|
||||
const lines: number[] = [];
|
||||
for (const child of todo.children) {
|
||||
lines.push(child.lineNumber);
|
||||
lines.push(...collectChildLineNumbers(child));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DOM element for a todo item with checkbox, right-click menu,
|
||||
* drag support, and recursive child rendering.
|
||||
*/
|
||||
export function createTodoItemEl(
|
||||
container: HTMLElement,
|
||||
@@ -15,12 +30,14 @@ export function createTodoItemEl(
|
||||
callbacks: TodoItemCallbacks
|
||||
): HTMLElement {
|
||||
const itemEl = container.createEl('li', { cls: 'todo-tracker-item' });
|
||||
itemEl.setAttribute('draggable', 'true');
|
||||
itemEl.dataset.lineNumber = String(todo.lineNumber);
|
||||
|
||||
// Checkbox
|
||||
const checkboxEl = itemEl.createEl('input', { type: 'checkbox' });
|
||||
checkboxEl.checked = todo.completed;
|
||||
checkboxEl.addEventListener('change', () => {
|
||||
callbacks.onToggle();
|
||||
callbacks.onToggle(todo);
|
||||
});
|
||||
|
||||
// Text content
|
||||
@@ -30,12 +47,9 @@ export function createTodoItemEl(
|
||||
textEl.addClass('todo-tracker-item-completed');
|
||||
}
|
||||
|
||||
// Menu button (ellipsis)
|
||||
const menuBtn = itemEl.createEl('button', { cls: 'todo-tracker-menu-btn' });
|
||||
setIcon(menuBtn, 'more-vertical');
|
||||
menuBtn.setAttribute('aria-label', 'Todo options');
|
||||
|
||||
menuBtn.addEventListener('click', (evt) => {
|
||||
// Right-click context menu
|
||||
itemEl.addEventListener('contextmenu', (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const menu = new Menu();
|
||||
@@ -44,12 +58,29 @@ export function createTodoItemEl(
|
||||
.setTitle('Move to...')
|
||||
.setIcon('arrow-right')
|
||||
.onClick(() => {
|
||||
callbacks.onMoveClick();
|
||||
callbacks.onMoveClick(todo);
|
||||
});
|
||||
});
|
||||
|
||||
menu.showAtMouseEvent(evt);
|
||||
});
|
||||
|
||||
// Drag events
|
||||
itemEl.addEventListener('dragstart', (evt) => {
|
||||
callbacks.onDragStart(evt, todo);
|
||||
});
|
||||
|
||||
itemEl.addEventListener('dragend', (evt) => {
|
||||
callbacks.onDragEnd(evt);
|
||||
});
|
||||
|
||||
// Render children recursively
|
||||
if (todo.children.length > 0) {
|
||||
const nestedList = itemEl.createEl('ul', { cls: 'todo-tracker-nested-list' });
|
||||
for (const child of todo.children) {
|
||||
createTodoItemEl(nestedList, child, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
return itemEl;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user