Files
obsidian-gui-tasks/src/view/TaskListComponent.ts
2026-03-28 10:55:42 -04:00

112 lines
3.2 KiB
TypeScript

import Sortable from 'sortablejs';
import type { Task } from '../types';
export interface TaskListCallbacks {
onComplete: (index: number) => void;
onEdit: (index: number) => void;
onReorder: (oldIndex: number, newIndex: number) => void;
onAdd: (text: string) => void;
}
/**
* Builds and manages the sortable task list DOM.
*/
export class TaskListComponent {
private container: HTMLElement;
private callbacks: TaskListCallbacks;
private sortable: Sortable | null = null;
constructor(container: HTMLElement, callbacks: TaskListCallbacks) {
this.container = container;
this.callbacks = callbacks;
}
render(tasks: Task[]): void {
this.destroy();
this.container.empty();
// Add-task row — always first, outside the sortable list
const addItem = this.container.createDiv({ cls: 'yaotp-task-item yaotp-add-task-item' });
const addHandle = addItem.createDiv({ cls: 'yaotp-drag-handle yaotp-drag-handle-disabled' });
addHandle.innerHTML = '⋮';
addItem.createEl('input', {
cls: 'yaotp-checkbox',
type: 'checkbox',
attr: { disabled: true, 'aria-hidden': 'true' },
});
const input = addItem.createEl('input', {
cls: 'yaotp-new-task-input',
type: 'text',
attr: { placeholder: 'Enter new task...' },
}) as HTMLInputElement;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const text = input.value.trim();
if (text) {
this.callbacks.onAdd(text);
}
}
});
if (tasks.length === 0) return;
const list = this.container.createEl('ul', { cls: 'yaotp-task-list' });
tasks.forEach((task, index) => {
const item = list.createEl('li', { cls: 'yaotp-task-item' });
if (task.completed) item.addClass('yaotp-task-completed');
item.dataset.index = String(index);
// Drag handle
const handle = item.createDiv({ cls: 'yaotp-drag-handle' });
handle.innerHTML = '⋮'; // vertical ellipsis ⋮
// Checkbox
const checkbox = item.createEl('input', {
cls: 'yaotp-checkbox',
type: 'checkbox',
attr: { 'aria-label': 'Complete task' },
}) as HTMLInputElement;
checkbox.checked = task.completed;
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
this.callbacks.onComplete(index);
});
// Task text (wraps)
const textEl = item.createDiv({ cls: 'yaotp-task-text' });
textEl.setText(task.text);
if (task.notes.length > 0) {
textEl.addClass('yaotp-task-has-notes');
}
textEl.addEventListener('click', () => {
this.callbacks.onEdit(index);
});
});
this.sortable = Sortable.create(list, {
handle: '.yaotp-drag-handle',
animation: 150,
delay: 300,
delayOnTouchOnly: true,
onEnd: (evt) => {
const oldIndex = evt.oldIndex;
const newIndex = evt.newIndex;
if (oldIndex !== undefined && newIndex !== undefined && oldIndex !== newIndex) {
this.callbacks.onReorder(oldIndex, newIndex);
}
},
});
}
destroy(): void {
if (this.sortable) {
this.sortable.destroy();
this.sortable = null;
}
}
}