tags
This commit is contained in:
1 parent
a9603aa0e8
commit
719f9d51ff
13 files changed
+625
-70
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// ── Context menu ──
|
||||
|
||||
import { $, esc, api } from './util.js';
|
||||
import { state, getChildren, isFavorite, toggleFavorite, CARD_COLORS } from './state.js';
|
||||
import { state, parseTags, getChildren, isFavorite, toggleFavorite, CARD_COLORS } from './state.js';
|
||||
import { showDialog } from './dialog.js';
|
||||
import { pushCardEdit, pushCardDelete, pushBatch, snapshotCard } from './undo.js';
|
||||
|
||||
@@ -10,6 +10,7 @@ const items = $('ctx-items');
|
||||
const colors = $('ctx-colors');
|
||||
const buckets = $('ctx-buckets');
|
||||
const members = $('ctx-members');
|
||||
const tags = $('ctx-tags');
|
||||
|
||||
export function hideContextMenu() { menu.classList.add('hidden'); }
|
||||
|
||||
@@ -25,10 +26,12 @@ export function showContextMenu(x, y, card) {
|
||||
<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="save-md"><i class="ph ph-file-md text-sm"></i><span class="font-pixel text-[10px]">save .md</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>
|
||||
<button class="ctx-item" data-action="tags"><i class="ph ph-hash text-sm"></i><span class="font-pixel text-[10px]">tags</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>
|
||||
@@ -60,11 +63,72 @@ export function showContextMenu(x, y, card) {
|
||||
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');
|
||||
|
||||
// Tag sub-panel
|
||||
const cardTags = parseTags(card);
|
||||
const allTags = [...new Set(state.cards.flatMap(c => parseTags(c)))].sort();
|
||||
const tagInput = $('ctx-tag-input');
|
||||
const tagList = $('ctx-tag-list');
|
||||
|
||||
function renderCtxTags(filter = '') {
|
||||
const f = filter.toLowerCase().replace(/^#/, '');
|
||||
const shown = f ? allTags.filter(t => t.includes(f)) : allTags;
|
||||
const hasCard = tag => cardTags.includes(tag);
|
||||
tagList.innerHTML = shown.map(t => `
|
||||
<button class="ctx-item ${hasCard(t) ? 'text-t-accent' : ''}" data-ctx-tag="${t}">
|
||||
<i class="ph ${hasCard(t) ? 'ph-check-square' : 'ph-square'} text-sm"></i>
|
||||
<span class="font-pixel text-[10px]">#${esc(t)}</span>
|
||||
</button>`).join('') +
|
||||
(f && !allTags.includes(f) ? `<button class="ctx-item text-t-dim" data-ctx-tag-new="${f}">
|
||||
<i class="ph ph-plus text-sm"></i><span class="font-pixel text-[10px]">add #${esc(f)}</span>
|
||||
</button>` : '');
|
||||
|
||||
tagList.querySelectorAll('[data-ctx-tag]').forEach(btn => btn.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const tag = btn.dataset.ctxTag;
|
||||
const has = cardTags.includes(tag);
|
||||
let newBody = card.body || '';
|
||||
if (has) {
|
||||
newBody = newBody.replace(new RegExp('\\s*#' + tag.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&') + '(?=\\s|$)', 'm'), '');
|
||||
cardTags.splice(cardTags.indexOf(tag), 1);
|
||||
} else {
|
||||
newBody = newBody.trimEnd() + ' #' + tag;
|
||||
cardTags.push(tag);
|
||||
}
|
||||
card.body = newBody;
|
||||
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { body: newBody } });
|
||||
const c = state.cards.find(x => x.id == card.id);
|
||||
if (c) { c.body = newBody; c.tags = cardTags.join(','); }
|
||||
renderCtxTags(tagInput.value);
|
||||
window._renderBoard();
|
||||
}));
|
||||
|
||||
tagList.querySelectorAll('[data-ctx-tag-new]').forEach(btn => btn.addEventListener('click', async e => {
|
||||
e.stopPropagation();
|
||||
const tag = btn.dataset.ctxTagNew;
|
||||
const newBody = (card.body || '').trimEnd() + ' #' + tag;
|
||||
card.body = newBody;
|
||||
cardTags.push(tag);
|
||||
allTags.push(tag);
|
||||
allTags.sort();
|
||||
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { body: newBody } });
|
||||
const c = state.cards.find(x => x.id == card.id);
|
||||
if (c) { c.body = newBody; c.tags = cardTags.join(','); }
|
||||
tagInput.value = '';
|
||||
renderCtxTags('');
|
||||
window._renderBoard();
|
||||
}));
|
||||
}
|
||||
|
||||
renderCtxTags();
|
||||
tagInput.value = '';
|
||||
tagInput.addEventListener('input', () => renderCtxTags(tagInput.value));
|
||||
tags.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'); };
|
||||
const toggle = (panel) => { [colors, buckets, members, tags].forEach(p => { if (p !== panel) p.classList.add('hidden'); }); panel.classList.toggle('hidden'); if (panel === tags && !tags.classList.contains('hidden')) setTimeout(() => tagInput.focus(), 0); };
|
||||
|
||||
// Actions
|
||||
items.querySelectorAll('[data-action]').forEach(btn => btn.addEventListener('click', async e => {
|
||||
@@ -73,9 +137,11 @@ export function showContextMenu(x, y, card) {
|
||||
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 === 'save-md') { hideContextMenu(); window.location.href = `api/export-md?ids=${card.id}`; }
|
||||
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 === 'tags') toggle(tags);
|
||||
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(); } }
|
||||
|
||||
Reference in new issue
Block a user