diff --git a/TaskTracker.Api/wwwroot/js/app.js b/TaskTracker.Api/wwwroot/js/app.js index 86b651a..6862bdf 100644 --- a/TaskTracker.Api/wwwroot/js/app.js +++ b/TaskTracker.Api/wwwroot/js/app.js @@ -1,2 +1,157 @@ -// TaskTracker app.js — command palette, keyboard shortcuts, drag-and-drop wiring -// Will be populated in later tasks +// TaskTracker app.js — drag-and-drop, detail panel, keyboard shortcuts + +(function() { + 'use strict'; + + // ======================================== + // Drag-and-Drop (SortableJS) + // ======================================== + + function initKanban() { + document.querySelectorAll('.kanban-column-body').forEach(function(el) { + // Destroy existing Sortable instance if re-initializing + if (el._sortable) { + el._sortable.destroy(); + } + + el._sortable = new Sortable(el, { + group: 'kanban', + animation: 150, + ghostClass: 'task-card--ghost', + dragClass: 'task-card--dragging', + handle: '.task-card', // Only drag by the card itself + filter: '.create-task-form', // Don't drag the create form + onEnd: function(evt) { + var taskId = evt.item.dataset.taskId; + var fromStatus = evt.from.dataset.status; + var toStatus = evt.to.dataset.status; + + // No-op if dropped in same column + if (fromStatus === toStatus) return; + + // Determine the handler based on transition + var handler = null; + if (toStatus === 'Active' && fromStatus === 'Paused') { + handler = 'Resume'; + } else if (toStatus === 'Active') { + handler = 'Start'; + } else if (toStatus === 'Paused' && fromStatus === 'Active') { + handler = 'Pause'; + } else if (toStatus === 'Completed') { + handler = 'Complete'; + } else { + // Invalid transition — revert by moving the item back + evt.from.appendChild(evt.item); + return; + } + + // Fire htmx request to update the task status + htmx.ajax('PUT', '/board?handler=' + handler + '&id=' + taskId, { + target: '#kanban-board', + swap: 'innerHTML' + }); + } + }); + }); + } + + // ======================================== + // Detail Panel helpers + // ======================================== + + // Defined on window so inline onclick handlers and partial scripts can call them. + // The _TaskDetail partial also defines these inline; whichever loads last wins, + // but the behavior is identical. + + window.closeDetailPanel = function() { + var overlay = document.querySelector('.detail-overlay'); + var panel = document.querySelector('.detail-panel'); + if (overlay) overlay.classList.remove('detail-overlay--open'); + if (panel) panel.classList.remove('detail-panel--open'); + // Clear content after CSS transition completes + setTimeout(function() { + var container = document.getElementById('detail-panel'); + if (container && !document.querySelector('.detail-panel--open')) { + container.innerHTML = ''; + } + }, 300); + }; + + window.startEdit = function(fieldId) { + var container = document.getElementById('edit-' + fieldId); + if (!container) return; + var display = container.querySelector('.inline-edit-display'); + var form = container.querySelector('.inline-edit-form'); + if (display) display.style.display = 'none'; + if (form) { + form.style.display = ''; + var input = form.querySelector('input, textarea'); + if (input) { + input.focus(); + if (input.type === 'text' || input.tagName === 'TEXTAREA') { + input.setSelectionRange(input.value.length, input.value.length); + } + } + } + }; + + window.cancelEdit = function(fieldId) { + var container = document.getElementById('edit-' + fieldId); + if (!container) return; + var display = container.querySelector('.inline-edit-display'); + var form = container.querySelector('.inline-edit-form'); + if (display) display.style.display = ''; + if (form) form.style.display = 'none'; + }; + + // ======================================== + // Initialization + // ======================================== + + // Initialize Kanban drag-and-drop on page load + document.addEventListener('DOMContentLoaded', function() { + initKanban(); + }); + + // Re-initialize after htmx swaps that affect the kanban board. + // This is critical because htmx replaces DOM nodes, destroying Sortable instances. + document.addEventListener('htmx:afterSwap', function(evt) { + var target = evt.detail.target; + if (target && (target.id === 'kanban-board' || target.id === 'board-content' || (target.closest && target.closest('#kanban-board')))) { + initKanban(); + } + }); + + // Also handle htmx:afterSettle for cases where the DOM isn't fully ready on afterSwap + document.addEventListener('htmx:afterSettle', function(evt) { + var target = evt.detail.target; + if (target && (target.id === 'kanban-board' || target.id === 'board-content')) { + initKanban(); + } + }); + + // Open detail panel when its content is loaded via htmx + document.addEventListener('htmx:afterSwap', function(evt) { + if (evt.detail.target && evt.detail.target.id === 'detail-panel') { + // Panel content was just loaded — trigger open animation + requestAnimationFrame(function() { + var overlay = document.querySelector('.detail-overlay'); + var panel = document.querySelector('.detail-panel'); + if (overlay) overlay.classList.add('detail-overlay--open'); + if (panel) panel.classList.add('detail-panel--open'); + }); + } + }); + + // Global Escape key handler for closing the detail panel + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + var panel = document.querySelector('.detail-panel--open'); + if (panel) { + closeDetailPanel(); + e.preventDefault(); + } + } + }); + +})();