diff --git a/public/app.js b/public/app.js
index 8bb7f2a..5ee2e41 100644
--- a/public/app.js
+++ b/public/app.js
@@ -50,6 +50,7 @@ class App {
async init() {
this.setupGreeting();
this.setupEventListeners();
+ this.setupContainerDrag();
await this.loadTasks();
await this.loadArchivedCount();
}
@@ -257,6 +258,30 @@ class App {
hideContextMenu() { document.getElementById('context-menu').classList.remove('show'); }
+ // ─── Confirm Dialog ───
+
+ showConfirm(message, icon = '🗑') {
+ return new Promise((resolve) => {
+ const overlay = document.getElementById('confirm-overlay');
+ document.getElementById('confirm-message').innerHTML = message;
+ document.getElementById('confirm-icon').innerHTML = icon;
+ overlay.classList.add('open');
+
+ const cancel = document.getElementById('confirm-cancel');
+ const del = document.getElementById('confirm-delete');
+
+ const cleanup = () => {
+ overlay.classList.remove('open');
+ cancel.replaceWith(cancel.cloneNode(true));
+ del.replaceWith(del.cloneNode(true));
+ };
+
+ document.getElementById('confirm-cancel').addEventListener('click', () => { cleanup(); resolve(false); });
+ document.getElementById('confirm-delete').addEventListener('click', () => { cleanup(); resolve(true); });
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) { cleanup(); resolve(false); } }, { once: true });
+ });
+ }
+
handleContextEdit() {
if (this.contextMenuTarget) this.openEditModal(this.contextMenuTarget, this.contextMenuParent);
this.hideContextMenu();
@@ -264,10 +289,14 @@ class App {
async handleContextDelete() {
if (!this.contextMenuTarget) return;
- if (!confirm(`Delete this ${this.contextMenuType}?`)) { this.hideContextMenu(); return; }
- if (this.contextMenuType === 'task') await this.deleteTask(this.contextMenuTarget.id);
- else if (this.contextMenuType === 'subtask' && this.contextMenuParent) await this.deleteSubtask(this.contextMenuParent.id, this.contextMenuTarget.id);
+ const type = this.contextMenuType;
this.hideContextMenu();
+ const confirmed = await this.showConfirm(
+ `Delete this ${type}?
This action cannot be undone.`
+ );
+ if (!confirmed) return;
+ if (type === 'task') this.deleteTask(this.contextMenuTarget.id);
+ else if (type === 'subtask' && this.contextMenuParent) this.deleteSubtask(this.contextMenuParent.id, this.contextMenuTarget.id);
}
async handleContextPromote() {
@@ -497,6 +526,13 @@ class App {
}
}
+ async confirmDeleteTask(taskId) {
+ const confirmed = await this.showConfirm(
+ 'Delete permanently?
This action cannot be undone.'
+ );
+ if (confirmed) this.deleteTask(taskId);
+ }
+
toggleTaskComplete(taskId) {
const task = this.getCurrentTasks().find(t => t.id === taskId);
if (!task) return;
@@ -589,26 +625,169 @@ class App {
}
}
- // ─── Drag & Drop ───
+ // ─── Drag & Drop (insertion indicator system) ───
+
+ _createDropIndicator() {
+ const el = document.createElement('div');
+ el.className = 'drop-indicator';
+ el.style.display = 'none';
+ document.body.appendChild(el);
+ return el;
+ }
+
+ _showDropIndicator(container, y) {
+ if (!this._dropIndicator) this._dropIndicator = this._createDropIndicator();
+ const cards = [...container.querySelectorAll(':scope > .task-card')];
+ let insertIndex = cards.length;
+ let indicatorY = 0;
+
+ for (let i = 0; i < cards.length; i++) {
+ const rect = cards[i].getBoundingClientRect();
+ const midY = rect.top + rect.height / 2;
+ if (y < midY) {
+ insertIndex = i;
+ indicatorY = rect.top - 4;
+ break;
+ }
+ }
+
+ // If after all cards
+ if (insertIndex === cards.length && cards.length > 0) {
+ const lastRect = cards[cards.length - 1].getBoundingClientRect();
+ indicatorY = lastRect.bottom + 4;
+ } else if (cards.length === 0) {
+ const containerRect = container.getBoundingClientRect();
+ indicatorY = containerRect.top + 8;
+ }
+
+ const containerRect = container.getBoundingClientRect();
+ this._dropIndicator.style.display = 'block';
+ this._dropIndicator.style.top = indicatorY + 'px';
+ this._dropIndicator.style.left = containerRect.left + 'px';
+ this._dropIndicator.style.width = containerRect.width + 'px';
+ this._dropInsertIndex = insertIndex;
+ }
+
+ _hideDropIndicator() {
+ if (this._dropIndicator) this._dropIndicator.style.display = 'none';
+ this._dropInsertIndex = null;
+ }
+
+ _showSubtaskDropIndicator(subtaskContainer, y) {
+ if (!this._subtaskDropIndicator) {
+ this._subtaskDropIndicator = document.createElement('div');
+ this._subtaskDropIndicator.className = 'drop-indicator drop-indicator-subtask';
+ this._subtaskDropIndicator.style.display = 'none';
+ document.body.appendChild(this._subtaskDropIndicator);
+ }
+ const subtasks = [...subtaskContainer.querySelectorAll(':scope > .subtask')];
+ let insertIndex = subtasks.length;
+ let indicatorY = 0;
+
+ for (let i = 0; i < subtasks.length; i++) {
+ const rect = subtasks[i].getBoundingClientRect();
+ if (y < rect.top + rect.height / 2) {
+ insertIndex = i;
+ indicatorY = rect.top - 2;
+ break;
+ }
+ }
+
+ if (insertIndex === subtasks.length && subtasks.length > 0) {
+ const lastRect = subtasks[subtasks.length - 1].getBoundingClientRect();
+ indicatorY = lastRect.bottom + 2;
+ }
+
+ const cRect = subtaskContainer.getBoundingClientRect();
+ this._subtaskDropIndicator.style.display = 'block';
+ this._subtaskDropIndicator.style.top = indicatorY + 'px';
+ this._subtaskDropIndicator.style.left = cRect.left + 'px';
+ this._subtaskDropIndicator.style.width = cRect.width + 'px';
+ this._subtaskDropInsertIndex = insertIndex;
+ }
+
+ _hideSubtaskDropIndicator() {
+ if (this._subtaskDropIndicator) this._subtaskDropIndicator.style.display = 'none';
+ this._subtaskDropInsertIndex = null;
+ }
+
+ setupContainerDrag() {
+ const container = document.getElementById('tasks-container');
+
+ container.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ if (!this.draggedTask) return;
+ this._showDropIndicator(container, e.clientY);
+ });
+
+ container.addEventListener('dragleave', (e) => {
+ // Only hide if leaving the container entirely
+ if (!container.contains(e.relatedTarget)) {
+ this._hideDropIndicator();
+ }
+ });
+
+ container.addEventListener('drop', (e) => {
+ e.preventDefault();
+
+ // Handle nesting via drop zone
+ const dropZone = e.target.closest('.drop-zone');
+ if (dropZone) {
+ const taskCard = dropZone.closest('.task-card');
+ if (taskCard) {
+ const taskId = taskCard.getAttribute('data-task-id');
+ const targetTask = this.tasks.find(t => t.id === taskId);
+ if (targetTask) this._handleNestDrop(targetTask);
+ }
+ this._hideDropIndicator();
+ return;
+ }
+
+ // Handle reorder via indicator
+ if (this.draggedTask && this._dropInsertIndex != null) {
+ const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
+ let targetIndex = this._dropInsertIndex;
+
+ // Adjust index since we're removing before inserting
+ if (currentIndex !== -1 && currentIndex < targetIndex) targetIndex--;
+ if (currentIndex !== -1 && currentIndex !== targetIndex) {
+ this.tasks.splice(currentIndex, 1);
+ this.tasks.splice(targetIndex, 0, this.draggedTask);
+ this._syncReorder();
+ this.render();
+ }
+ }
+
+ this._hideDropIndicator();
+ });
+ }
handleDragStart(e, item, isSubtask = false, parentTask = null, index = null) {
- if (isSubtask) { this.draggedSubtask = item; this.draggedParent = parentTask; this.draggedSubtaskIndex = index; this.draggedTask = null; }
- else { this.draggedTask = item; this.draggedSubtask = null; this.draggedParent = null; this.draggedSubtaskIndex = null; }
+ if (isSubtask) {
+ this.draggedSubtask = item; this.draggedParent = parentTask;
+ this.draggedSubtaskIndex = index; this.draggedTask = null;
+ } else {
+ this.draggedTask = item; this.draggedSubtask = null;
+ this.draggedParent = null; this.draggedSubtaskIndex = null;
+ }
e.currentTarget.classList.add('dragging');
+ // Delay to let browser capture drag image
+ requestAnimationFrame(() => e.currentTarget.style.opacity = '0.4');
}
handleDragEnd(e) {
e.currentTarget.classList.remove('dragging');
+ e.currentTarget.style.opacity = '';
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('visible'));
+ this._hideDropIndicator();
+ this._hideSubtaskDropIndicator();
if (this.dropZoneTimer) { clearTimeout(this.dropZoneTimer); this.dropZoneTimer = null; }
- this.draggedTask = null; this.draggedSubtask = null; this.draggedParent = null; this.draggedSubtaskIndex = null;
+ this.draggedTask = null; this.draggedSubtask = null;
+ this.draggedParent = null; this.draggedSubtaskIndex = null;
}
- handleCardDragOver(e) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.add('drag-over'); }
- handleDragLeave(e) { e.currentTarget.classList.remove('drag-over'); }
-
- handleDragEnter(e, taskId) {
+ handleDragEnterCard(e, taskId) {
e.preventDefault();
if (!this.draggedTask && !this.draggedSubtask) return;
if (this.dropZoneTimer) clearTimeout(this.dropZoneTimer);
@@ -618,8 +797,7 @@ class App {
}, 1000);
}
- async handleDrop(e, targetTask) {
- e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
+ _handleNestDrop(targetTask) {
if (this.draggedTask && this.draggedTask.id !== targetTask.id) {
targetTask.subtasks.push(this.draggedTask);
this.tasks = this.tasks.filter(t => t.id !== this.draggedTask.id);
@@ -637,58 +815,34 @@ class App {
}
}
- async handleTaskReorder(e, targetIndex) {
- e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
- if (!this.draggedTask) return;
- const currentIndex = this.tasks.findIndex(t => t.id === this.draggedTask.id);
- if (currentIndex === -1 || currentIndex === targetIndex) return;
-
- // Update data
- this.tasks.splice(currentIndex, 1);
- this.tasks.splice(targetIndex, 0, this.draggedTask);
-
- // Swap DOM nodes directly
- const container = document.getElementById('tasks-container');
- const cards = [...container.children];
- const draggedCard = cards[currentIndex];
- if (draggedCard) {
- draggedCard.remove();
- const referenceCard = container.children[targetIndex];
- if (referenceCard) container.insertBefore(draggedCard, referenceCard);
- else container.appendChild(draggedCard);
- }
-
- // Sync to server in background
- this._syncReorder();
+ handleSubtaskContainerDragOver(e, task) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== task.id) return;
+ const subtaskContainer = e.currentTarget;
+ this._showSubtaskDropIndicator(subtaskContainer, e.clientY);
}
- async handleSubtaskReorder(e, parentTask, targetIndex) {
- e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('drag-over');
- if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== parentTask.id) return;
- if (this.draggedSubtaskIndex === targetIndex) return;
-
- // Update data
- const subtask = parentTask.subtasks[this.draggedSubtaskIndex];
- parentTask.subtasks.splice(this.draggedSubtaskIndex, 1);
- parentTask.subtasks.splice(targetIndex, 0, subtask);
-
- // Swap subtask DOM nodes
- const card = document.querySelector(`[data-task-id="${parentTask.id}"]`);
- if (card) {
- const subtaskContainer = card.querySelector('.subtasks-container');
- if (subtaskContainer) {
- const subtaskNodes = [...subtaskContainer.children];
- const draggedNode = subtaskNodes[this.draggedSubtaskIndex];
- if (draggedNode) {
- draggedNode.remove();
- const ref = subtaskContainer.children[targetIndex];
- if (ref) subtaskContainer.insertBefore(draggedNode, ref);
- else subtaskContainer.appendChild(draggedNode);
- }
- }
+ handleSubtaskContainerDrop(e, task) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!this.draggedSubtask || !this.draggedParent || this.draggedParent.id !== task.id) {
+ this._hideSubtaskDropIndicator();
+ return;
}
+ if (this._subtaskDropInsertIndex == null) { this._hideSubtaskDropIndicator(); return; }
- this._syncToServer(parentTask);
+ let targetIndex = this._subtaskDropInsertIndex;
+ const currentIndex = this.draggedSubtaskIndex;
+ if (currentIndex < targetIndex) targetIndex--;
+ if (currentIndex !== targetIndex) {
+ const subtask = task.subtasks[currentIndex];
+ task.subtasks.splice(currentIndex, 1);
+ task.subtasks.splice(targetIndex, 0, subtask);
+ this._syncToServer(task);
+ this.render();
+ }
+ this._hideSubtaskDropIndicator();
}
// ─── Render: Active ───
@@ -702,15 +856,9 @@ class App {
card.setAttribute('data-task-id', task.id);
card.style.borderLeftColor = task.color || '#6366f1';
- card.addEventListener('dragstart', (e) => this.handleDragStart(e, task, false));
+ card.addEventListener('dragstart', (e) => { if (!e.target.closest('.subtask')) this.handleDragStart(e, task, false); });
card.addEventListener('dragend', (e) => this.handleDragEnd(e));
- card.addEventListener('dragover', (e) => this.handleCardDragOver(e));
- card.addEventListener('dragleave', (e) => this.handleDragLeave(e));
- card.addEventListener('dragenter', (e) => this.handleDragEnter(e, task.id));
- card.addEventListener('drop', (e) => {
- if (e.target.closest('.drop-zone')) this.handleDrop(e, task);
- else if (this.draggedTask && this.draggedTask.id !== task.id) this.handleTaskReorder(e, index);
- });
+ card.addEventListener('dragenter', (e) => this.handleDragEnterCard(e, task.id));
card.addEventListener('contextmenu', (e) => { if (!e.target.closest('.subtask')) this.showContextMenu(e, task, 'task'); });
card.addEventListener('dblclick', (e) => { if (!e.target.closest('.subtask') && !e.target.closest('button')) this.openEditModal(task); });
@@ -775,6 +923,13 @@ class App {
const container = document.createElement('div');
container.className = 'subtasks-container';
+ // Container-level drag events for subtask reorder
+ container.addEventListener('dragover', (e) => this.handleSubtaskContainerDragOver(e, task));
+ container.addEventListener('drop', (e) => this.handleSubtaskContainerDrop(e, task));
+ container.addEventListener('dragleave', (e) => {
+ if (!container.contains(e.relatedTarget)) this._hideSubtaskDropIndicator();
+ });
+
task.subtasks.forEach((subtask, index) => {
const div = document.createElement('div');
div.className = 'subtask';
@@ -783,12 +938,6 @@ class App {
div.addEventListener('dragstart', (e) => { e.stopPropagation(); this.handleDragStart(e, subtask, true, task, index); });
div.addEventListener('dragend', (e) => { e.stopPropagation(); this.handleDragEnd(e); });
- div.addEventListener('dragover', (e) => {
- e.preventDefault(); e.stopPropagation();
- if (this.draggedSubtask && this.draggedParent && this.draggedParent.id === task.id) e.currentTarget.classList.add('drag-over');
- });
- div.addEventListener('dragleave', (e) => { e.stopPropagation(); e.currentTarget.classList.remove('drag-over'); });
- div.addEventListener('drop', (e) => { e.stopPropagation(); this.handleSubtaskReorder(e, task, index); });
div.addEventListener('contextmenu', (e) => { e.stopPropagation(); this.showContextMenu(e, subtask, 'subtask', task); });
div.addEventListener('dblclick', (e) => { e.stopPropagation(); if (!e.target.closest('button')) this.openEditModal(subtask, task); });
@@ -878,7 +1027,7 @@ class App {
` : ''}