Files
hyprcrd/public/js/modules/context-menu.js
T
2026-04-07 17:47:09 +02:00

95 lines
8.2 KiB
JavaScript

// ── Context menu ──
import { $, esc, api } from './util.js';
import { state, getChildren, isFavorite, toggleFavorite, 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>
<button class="ctx-item" data-action="favorite"><i class="ph${isFavorite(card.id) ? '-fill' : ''} ph-star text-sm ${isFavorite(card.id) ? 'text-amber-400' : ''}"></i><span class="font-pixel text-[10px]">${isFavorite(card.id) ? 'unfavorite' : 'favorite'}</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 === 'favorite') { hideContextMenu(); toggleFavorite(card.id); window._renderBoard(); }
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(); }));
}