/* kanban-settings.js — settings panel: add/rename/delete/reorder columns and lanes */ (function () { 'use strict'; var boardId = KANBAN.boardId; var panel = document.getElementById('settings-panel'); var overlay = document.getElementById('settings-overlay'); /* ── Panel open/close ────────────────────────────────────── */ document.getElementById('btn-settings').addEventListener('click', openPanel); document.getElementById('btn-close-settings').addEventListener('click', closePanel); overlay.addEventListener('click', closePanel); function openPanel() { panel.classList.add('open'); overlay.classList.remove('d-none'); } function closePanel() { panel.classList.remove('open'); overlay.classList.add('d-none'); } /* ── Helpers ─────────────────────────────────────────────── */ function post(url, data, cb) { var params = new URLSearchParams(); Object.keys(data).forEach(function (k) { params.append(k, data[k]); }); fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() }) .then(function (r) { return r.json(); }) .then(cb) .catch(function (e) { console.error(url, e); }); } function postJson(url, payload, cb) { fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(function (r) { return r.json(); }).then(cb) .catch(function (e) { console.error(url, e); }); } function collectOrder(listId) { return Array.from(document.querySelectorAll('#' + listId + ' li')).map(function (li, idx) { return { id: parseInt(li.dataset.id), position: idx }; }); } function buildListItem(id, name, editClass, deleteClass, labelClass) { var li = document.createElement('li'); li.className = 'list-group-item d-flex align-items-center gap-2 py-2'; li.dataset.id = id; li.innerHTML = '' + '' + esc(name) + '' + '' + ''; return li; } function esc(s) { return String(s) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /* ── Sortable reorder ─────────────────────────────────────── */ function initSortable(listId, reorderUrl) { var el = document.getElementById(listId); Sortable.create(el, { handle: '.drag-handle', animation: 150, onEnd: function () { postJson(reorderUrl, collectOrder(listId), function (res) { if (!res.ok) console.error('Reorder failed', res); }); } }); } initSortable('col-list', '/columns/reorder'); initSortable('lane-list', '/swimlanes/reorder'); /* ═══════════════════════════════════════════════════════════ COLUMNS ═══════════════════════════════════════════════════════════ */ /* ── Add column ───────────────────────────────────────────── */ document.getElementById('btn-add-column').addEventListener('click', function () { document.getElementById('col-add-form').classList.remove('d-none'); document.getElementById('col-add-input').focus(); }); document.getElementById('btn-col-add-cancel').addEventListener('click', function () { document.getElementById('col-add-form').classList.add('d-none'); document.getElementById('col-add-input').value = ''; }); document.getElementById('btn-col-add-save').addEventListener('click', function () { var name = document.getElementById('col-add-input').value.trim(); if (!name) return; post('/columns', { board_id: boardId, name: name }, function (res) { if (!res.ok) { alert(res.error || 'Failed'); return; } document.getElementById('col-add-form').classList.add('d-none'); document.getElementById('col-add-input').value = ''; var li = buildListItem(res.id, res.name, 'btn-edit-col', 'btn-delete-col', 'col-label-text'); document.getElementById('col-list').appendChild(li); bindColItem(li); window.KanbanBoard.addColumn(res); }); }); /* ── Bind edit/delete on existing column items ─────────────── */ function bindColItem(li) { li.querySelector('.btn-edit-col').addEventListener('click', function () { startRename(li, '.col-label-text', function (newName, done) { post('/columns/' + li.dataset.id, { name: newName }, function (res) { if (res.ok) { done(true); window.KanbanBoard.renameColumn(li.dataset.id, newName); } else { done(false); alert(res.error || 'Rename failed'); } }); }); }); li.querySelector('.btn-delete-col').addEventListener('click', function () { if (!confirm('Delete this column and all its cards?')) return; post('/columns/' + li.dataset.id + '/delete', {}, function (res) { if (res.ok) { window.KanbanBoard.removeColumn(li.dataset.id); li.remove(); } else { alert(res.error || 'Delete failed'); } }); }); } document.querySelectorAll('#col-list li').forEach(bindColItem); /* ═══════════════════════════════════════════════════════════ SWIM LANES ═══════════════════════════════════════════════════════════ */ /* ── Add lane ─────────────────────────────────────────────── */ document.getElementById('btn-add-lane').addEventListener('click', function () { document.getElementById('lane-add-form').classList.remove('d-none'); document.getElementById('lane-add-input').focus(); }); document.getElementById('btn-lane-add-cancel').addEventListener('click', function () { document.getElementById('lane-add-form').classList.add('d-none'); document.getElementById('lane-add-input').value = ''; }); document.getElementById('btn-lane-add-save').addEventListener('click', function () { var name = document.getElementById('lane-add-input').value.trim(); if (!name) return; post('/swimlanes', { board_id: boardId, name: name }, function (res) { if (!res.ok) { alert(res.error || 'Failed'); return; } document.getElementById('lane-add-form').classList.add('d-none'); document.getElementById('lane-add-input').value = ''; var li = buildListItem(res.id, res.name, 'btn-edit-lane', 'btn-delete-lane', 'lane-label-text'); document.getElementById('lane-list').appendChild(li); bindLaneItem(li); window.KanbanBoard.addLane(res); }); }); /* ── Bind edit/delete on existing lane items ──────────────── */ function bindLaneItem(li) { li.querySelector('.btn-edit-lane').addEventListener('click', function () { startRename(li, '.lane-label-text', function (newName, done) { post('/swimlanes/' + li.dataset.id, { name: newName }, function (res) { if (res.ok) { done(true); window.KanbanBoard.renameLane(li.dataset.id, newName); } else { done(false); alert(res.error || 'Rename failed'); } }); }); }); li.querySelector('.btn-delete-lane').addEventListener('click', function () { if (!confirm('Delete this swim lane and all its cards?')) return; post('/swimlanes/' + li.dataset.id + '/delete', {}, function (res) { if (res.ok) { window.KanbanBoard.removeLane(li.dataset.id); li.remove(); } else { alert(res.error || 'Delete failed'); } }); }); } document.querySelectorAll('#lane-list li').forEach(bindLaneItem); /* ── Inline rename helper ─────────────────────────────────── */ function startRename(li, labelSel, saveCb) { var span = li.querySelector(labelSel); var oldName = span.textContent.trim(); var input = document.createElement('input'); input.type = 'text'; input.className = 'form-control form-control-sm inline-rename flex-grow-1'; input.value = oldName; span.replaceWith(input); input.focus(); input.select(); function commit() { var newName = input.value.trim(); if (!newName || newName === oldName) { abort(); return; } saveCb(newName, function (ok) { var replacement = document.createElement('span'); replacement.className = labelSel.replace('.', '') + ' flex-grow-1'; replacement.textContent = ok ? newName : oldName; input.replaceWith(replacement); }); } function abort() { var replacement = document.createElement('span'); replacement.className = labelSel.replace('.', '') + ' flex-grow-1'; replacement.textContent = oldName; input.replaceWith(replacement); } input.addEventListener('blur', commit); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); commit(); } if (e.key === 'Escape') { e.preventDefault(); abort(); } }); } })();