Bring Nexus-style taskboard interactions to Maxi MC
This commit is contained in:
+91
-16
@@ -142,9 +142,11 @@ function boardView() {
|
||||
function column(name) {
|
||||
const tasks = state.tasks.filter((task) => task.state === name);
|
||||
return `
|
||||
<section class="column" data-state="${name}">
|
||||
<h2>${name}<span class="count">${tasks.length}</span></h2>
|
||||
${tasks.map(taskCard).join("") || `<p class="meta">Keine Aufgaben</p>`}
|
||||
<section class="column" data-state="${name}" aria-label="${name}">
|
||||
<h2><span>${name}</span><span class="count">${tasks.length}</span></h2>
|
||||
<div class="dropzone" data-state="${name}">
|
||||
${tasks.map(taskCard).join("") || `<p class="empty-state">Keine Aufgaben</p>`}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -152,20 +154,23 @@ function column(name) {
|
||||
function taskCard(task) {
|
||||
const index = states.indexOf(task.state);
|
||||
return `
|
||||
<article class="task">
|
||||
<article class="task priority-${escapeHtml(task.priority).toLowerCase()}" draggable="true" data-task-id="${task.id}" tabindex="0" role="button" aria-label="Aufgabe ${escapeHtml(task.title)} oeffnen">
|
||||
<div class="task-card-head">
|
||||
<span class="badge">${escapeHtml(task.priority)}</span>
|
||||
<span class="meta">${escapeHtml(task.assignee || "luna")}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3>${escapeHtml(task.title)}</h3>
|
||||
${task.detail ? `<p>${escapeHtml(task.detail)}</p>` : ""}
|
||||
</div>
|
||||
<div class="task-footer">
|
||||
<span class="badge">${escapeHtml(task.priority)}</span>
|
||||
<time class="meta" datetime="${escapeHtml(task.updatedAt)}">${formatDate(task.updatedAt)}</time>
|
||||
<div class="task-controls">
|
||||
<button class="icon-button" data-move="${task.id}" data-state="${states[Math.max(0, index - 1)]}" title="Zurueck">‹</button>
|
||||
<button class="icon-button" data-edit="${task.id}" title="Bearbeiten">✎</button>
|
||||
<button class="icon-button" data-move="${task.id}" data-state="${states[Math.min(states.length - 1, index + 1)]}" title="Weiter">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="meta">${escapeHtml(task.assignee || "luna")}</span>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
@@ -175,6 +180,7 @@ function taskDialog() {
|
||||
<dialog id="task-dialog">
|
||||
<form id="task-form">
|
||||
<h2 id="dialog-title">Aufgabe</h2>
|
||||
<div class="dialog-meta" id="dialog-meta"></div>
|
||||
<input type="hidden" name="id">
|
||||
<label class="field">Titel
|
||||
<input name="title" required>
|
||||
@@ -205,29 +211,93 @@ function taskDialog() {
|
||||
function bindBoard() {
|
||||
const dialog = document.querySelector("#task-dialog");
|
||||
const form = document.querySelector("#task-form");
|
||||
document.querySelector("#new-task").addEventListener("click", () => {
|
||||
const dialogTitle = document.querySelector("#dialog-title");
|
||||
const dialogMeta = document.querySelector("#dialog-meta");
|
||||
let draggedTaskId = null;
|
||||
const resetDialog = () => {
|
||||
form.reset();
|
||||
form.elements.id.value = "";
|
||||
form.elements.assignee.value = state.settings?.defaultAssignee || "luna";
|
||||
dialogTitle.textContent = "Neue Aufgabe";
|
||||
dialogMeta.textContent = "";
|
||||
};
|
||||
const openTaskDialog = (task) => {
|
||||
form.elements.id.value = task.id;
|
||||
form.elements.title.value = task.title;
|
||||
form.elements.detail.value = task.detail || "";
|
||||
form.elements.state.value = task.state;
|
||||
form.elements.priority.value = task.priority;
|
||||
form.elements.assignee.value = task.assignee || "";
|
||||
dialogTitle.textContent = task.title;
|
||||
dialogMeta.textContent = `Status ${task.state} · Aktualisiert ${formatDate(task.updatedAt)} · Erstellt ${formatDate(task.createdAt)}`;
|
||||
dialog.showModal();
|
||||
};
|
||||
document.querySelector("#new-task").addEventListener("click", () => {
|
||||
resetDialog();
|
||||
dialog.showModal();
|
||||
});
|
||||
document.querySelector("#cancel-task").addEventListener("click", () => dialog.close());
|
||||
dialog.addEventListener("click", (event) => {
|
||||
if (event.target === dialog) dialog.close();
|
||||
});
|
||||
document.querySelectorAll("[data-move]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
button.addEventListener("click", async (event) => {
|
||||
event.stopPropagation();
|
||||
await api(`/api/tasks/${button.dataset.move}`, { method: "PATCH", body: JSON.stringify({ state: button.dataset.state }) });
|
||||
await loadTasks();
|
||||
renderApp();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-edit]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
button.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
const task = state.tasks.find((x) => x.id === button.dataset.edit);
|
||||
form.elements.id.value = task.id;
|
||||
form.elements.title.value = task.title;
|
||||
form.elements.detail.value = task.detail || "";
|
||||
form.elements.state.value = task.state;
|
||||
form.elements.priority.value = task.priority;
|
||||
form.elements.assignee.value = task.assignee || "";
|
||||
dialog.showModal();
|
||||
if (task) openTaskDialog(task);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("[data-task-id]").forEach((card) => {
|
||||
card.addEventListener("click", (event) => {
|
||||
if (event.target.closest("button, input, textarea, select, a")) return;
|
||||
const task = state.tasks.find((x) => x.id === card.dataset.taskId);
|
||||
if (task) openTaskDialog(task);
|
||||
});
|
||||
card.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
event.preventDefault();
|
||||
const task = state.tasks.find((x) => x.id === card.dataset.taskId);
|
||||
if (task) openTaskDialog(task);
|
||||
});
|
||||
card.addEventListener("dragstart", (event) => {
|
||||
draggedTaskId = card.dataset.taskId;
|
||||
card.classList.add("dragging");
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", draggedTaskId);
|
||||
});
|
||||
card.addEventListener("dragend", () => {
|
||||
draggedTaskId = null;
|
||||
card.classList.remove("dragging");
|
||||
document.querySelectorAll(".dropzone.drag-over").forEach((zone) => zone.classList.remove("drag-over"));
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".dropzone").forEach((zone) => {
|
||||
zone.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
zone.classList.add("drag-over");
|
||||
});
|
||||
zone.addEventListener("dragleave", (event) => {
|
||||
if (!zone.contains(event.relatedTarget)) zone.classList.remove("drag-over");
|
||||
});
|
||||
zone.addEventListener("drop", async (event) => {
|
||||
event.preventDefault();
|
||||
zone.classList.remove("drag-over");
|
||||
const taskId = event.dataTransfer.getData("text/plain") || draggedTaskId;
|
||||
const nextState = zone.dataset.state;
|
||||
const task = state.tasks.find((x) => x.id === taskId);
|
||||
if (!task || task.state === nextState) return;
|
||||
await api(`/api/tasks/${taskId}`, { method: "PATCH", body: JSON.stringify({ state: nextState }) });
|
||||
await loadTasks();
|
||||
renderApp();
|
||||
});
|
||||
});
|
||||
form.addEventListener("submit", async (event) => {
|
||||
@@ -299,4 +369,9 @@ function escapeHtml(value) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return "unbekannt";
|
||||
return new Date(value).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" });
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user