93 lines
7.8 KiB
JavaScript
93 lines
7.8 KiB
JavaScript
// ── Context menu ──
|
|
|
|
import { $, esc, api } from './util.js';
|
|
import { state, getChildren, CARD_COLORS } from './state.js';
|
|
import { showDialog } from './dialog.js';
|
|
import { pushCardEdit, pushCardDelete, pushBatch, snapshotCard } from './undo.js';
|
|
|
|
const menu = $('ctx-menu');
|
|
const items = $('ctx-items');
|
|
const colors = $('ctx-colors');
|
|
const buckets = $('ctx-buckets');
|
|
const members = $('ctx-members');
|
|
|
|
export function hideContextMenu() { menu.classList.add('hidden'); }
|
|
|
|
document.addEventListener('click', hideContextMenu);
|
|
document.addEventListener('contextmenu', e => { if (!menu.contains(e.target)) hideContextMenu(); });
|
|
|
|
export function showContextMenu(x, y, card) {
|
|
const children = getChildren(card.id);
|
|
const isParent = children.length > 0;
|
|
const isChild = !!card.parent_id;
|
|
|
|
items.innerHTML = `
|
|
<button class="ctx-item" data-action="open"><i class="ph ph-eye text-sm"></i><span class="font-pixel text-[10px]">open</span></button>
|
|
<button class="ctx-item" data-action="edit"><i class="ph ph-pencil-simple text-sm"></i><span class="font-pixel text-[10px]">edit</span></button>
|
|
<button class="ctx-item" data-action="duplicate"><i class="ph ph-copy text-sm"></i><span class="font-pixel text-[10px]">duplicate</span></button>
|
|
<div class="ctx-sep"></div>
|
|
<button class="ctx-item" data-action="color"><i class="ph ph-palette text-sm"></i><span class="font-pixel text-[10px]">color</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
|
<button class="ctx-item" data-action="bucket"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">bucket</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
|
${isParent ? `<div class="ctx-sep"></div>
|
|
<button class="ctx-item" data-action="expand-group"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">open group</span></button>
|
|
<button class="ctx-item" data-action="group-members"><i class="ph ph-list text-sm"></i><span class="font-pixel text-[10px]">members (${children.length + 1})</span><i class="ph ph-caret-right text-xs ml-auto"></i></button>
|
|
<button class="ctx-item" data-action="dissolve"><i class="ph ph-arrow-line-up text-sm"></i><span class="font-pixel text-[10px]">dissolve group</span></button>` : ''}
|
|
${isChild ? `<div class="ctx-sep"></div>
|
|
<button class="ctx-item" data-action="go-to-group"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">go to group</span></button>
|
|
<button class="ctx-item" data-action="ungroup"><i class="ph ph-arrow-line-up text-sm"></i><span class="font-pixel text-[10px]">ungroup</span></button>` : ''}
|
|
<div class="ctx-sep"></div>
|
|
<button class="ctx-item danger" data-action="delete"><i class="ph ph-trash text-sm"></i><span class="font-pixel text-[10px]">delete</span></button>`;
|
|
|
|
// Color sub-panel
|
|
const grid = $('ctx-color-grid');
|
|
grid.innerHTML = CARD_COLORS.map(c => `<button class="w-5 h-5 rounded-sm border-2 transition-all hover:scale-125 ${c.hex === card.color ? 'border-t-bright' : 'border-transparent'}" style="background:${c.hex}" data-ctx-color="${c.hex}"></button>`).join('');
|
|
colors.classList.add('hidden');
|
|
|
|
// Members sub-panel
|
|
if (isParent) {
|
|
members.innerHTML = [card, ...children].map(gc => `
|
|
<button class="ctx-item" data-ctx-member="${gc.id}">
|
|
<span class="w-2 h-2 rounded-full shrink-0" style="background:${gc.color}"></span>
|
|
<span class="font-pixel text-[10px] truncate flex-1">${esc(gc.title)}</span>
|
|
${gc.id !== card.id ? `<i class="ph ph-arrow-line-up text-[10px] text-t-dim hover:text-t-danger" data-ctx-kick="${gc.id}"></i>` : ''}
|
|
</button>`).join('');
|
|
}
|
|
members.classList.add('hidden');
|
|
|
|
// Bucket sub-panel
|
|
buckets.innerHTML = `<button class="ctx-item ${!card.bucket_id ? 'text-t-accent' : ''}" data-ctx-bucket=""><i class="ph ph-x text-sm"></i><span class="font-pixel text-[10px]">no bucket</span></button>` +
|
|
state.buckets.map(b => `<button class="ctx-item ${card.bucket_id == b.id ? 'text-t-accent' : ''}" data-ctx-bucket="${b.id}"><i class="ph ph-folder text-sm"></i><span class="font-pixel text-[10px]">${esc(b.name)}</span></button>`).join('');
|
|
buckets.classList.add('hidden');
|
|
|
|
menu.style.left = Math.min(x, innerWidth - 188) + 'px';
|
|
menu.style.top = Math.min(y, innerHeight - 300) + 'px';
|
|
menu.classList.remove('hidden');
|
|
|
|
const toggle = (panel) => { [colors, buckets, members].forEach(p => { if (p !== panel) p.classList.add('hidden'); }); panel.classList.toggle('hidden'); };
|
|
|
|
// Actions
|
|
items.querySelectorAll('[data-action]').forEach(btn => btn.addEventListener('click', async e => {
|
|
e.stopPropagation();
|
|
const a = btn.dataset.action;
|
|
if (a === 'open') { hideContextMenu(); window._openDetail(card.id); }
|
|
else if (a === 'edit') { hideContextMenu(); window._openModal(card); }
|
|
else if (a === 'duplicate') { hideContextMenu(); await api('api/cards', { method: 'POST', body: { title: card.title + ' (copy)', body: card.body, color: card.color, bucket_id: card.bucket_id, position: -1 } }); await window._reloadAll(); }
|
|
else if (a === 'color') toggle(colors);
|
|
else if (a === 'bucket') toggle(buckets);
|
|
else if (a === 'group-members') toggle(members);
|
|
else if (a === 'expand-group') { hideContextMenu(); state.activeGroup = card.id; window._renderBoard(); }
|
|
else if (a === 'go-to-group') { hideContextMenu(); const p = state.cards.find(c => c.id == card.parent_id); if (p) { state.activeGroup = p.id; window._renderBoard(); } }
|
|
else if (a === 'dissolve') {
|
|
hideContextMenu(); const ok = await showDialog({ title: 'dissolve_group', message: `Gruppe "${card.title}" aufloesen?`, danger: true, okLabel: 'dissolve' });
|
|
if (ok) { const ch = getChildren(card.id); pushBatch('dissolve', ch.map(c => ({ cardId: c.id, prevData: { parent_id: card.id } }))); await Promise.all(ch.map(c => api(`api/cards/${c.id}`, { method: 'PATCH', body: { parent_id: null } }))); await window._reloadAll(); }
|
|
}
|
|
else if (a === 'ungroup') { hideContextMenu(); pushCardEdit('ungroup', card.id, { parent_id: card.parent_id }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { parent_id: null } }); await window._reloadAll(); }
|
|
else if (a === 'delete') { hideContextMenu(); const ok = await showDialog({ title: 'delete_card', message: `"${card.title}" wirklich loeschen?`, danger: true, okLabel: 'delete' }); if (ok) { pushCardDelete('delete', snapshotCard(card)); await api(`api/cards/${card.id}`, { method: 'DELETE' }); await window._reloadAll(); } }
|
|
}));
|
|
|
|
grid.querySelectorAll('[data-ctx-color]').forEach(btn => btn.addEventListener('click', async e => { e.stopPropagation(); hideContextMenu(); pushCardEdit('color', card.id, { color: card.color }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { color: btn.dataset.ctxColor } }); await window._reloadAll(); }));
|
|
buckets.querySelectorAll('[data-ctx-bucket]').forEach(btn => btn.addEventListener('click', async e => { e.stopPropagation(); hideContextMenu(); pushCardEdit('bucket', card.id, { bucket_id: card.bucket_id }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { bucket_id: btn.dataset.ctxBucket || null } }); await window._reloadAll(); }));
|
|
members.querySelectorAll('[data-ctx-member]').forEach(btn => btn.addEventListener('click', e => { e.stopPropagation(); if (e.target.closest('[data-ctx-kick]')) return; hideContextMenu(); window._openDetail(btn.dataset.ctxMember); }));
|
|
members.querySelectorAll('[data-ctx-kick]').forEach(btn => btn.addEventListener('click', async e => { e.stopPropagation(); hideContextMenu(); await api(`api/cards/${btn.dataset.ctxKick}`, { method: 'PATCH', body: { parent_id: null } }); await window._reloadAll(); }));
|
|
}
|