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; } } }