112 lines
3.2 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|