diff --git a/public/index.php b/public/index.php index fa87a79..88ef4ed 100644 --- a/public/index.php +++ b/public/index.php @@ -720,12 +720,6 @@ if (str_starts_with($route, '/api/')) { - - diff --git a/public/js/modules/board.js b/public/js/modules/board.js index f404225..7d89ab5 100644 --- a/public/js/modules/board.js +++ b/public/js/modules/board.js @@ -448,18 +448,26 @@ async function handleCardDrop(e, el, targetId) { return; } - // Compute new order + // Compute new order — must mirror renderBoard's filtering let filtered; if (state.activeGroup) { const parent = state.cards.find(c => c.id == state.activeGroup); filtered = parent ? sortedCards([parent, ...getChildren(parent.id)]) : []; } else { - filtered = sortedCards(state.cards.filter(c => { - if (c.parent_id) return false; - if (state.activeBucket === 'none') return !c.bucket_id; - if (state.activeBucket !== 'all') return c.bucket_id == state.activeBucket; - return true; - })); + const root = state.cards.filter(c => !c.parent_id); + if (state.activeBucket === 'favorites') { + const favs = getFavorites(); + filtered = sortedCards(root.filter(c => favs.includes(String(c.id)))); + } else if (state.activeBucket === 'journal') { + filtered = root.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title)); + filtered.sort((a, b) => b.title.localeCompare(a.title)); + } else if (state.activeBucket === 'none') { + filtered = sortedCards(root.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title))); + } else if (state.activeBucket !== 'all' && state.activeBucket !== 'current') { + filtered = sortedCards(root.filter(c => c.bucket_id == state.activeBucket)); + } else { + filtered = sortedCards(root); + } } const ids = filtered.map(c => String(c.id)).filter(i => i !== String(dragId)); ids.splice(wasBefore ? ids.indexOf(String(targetId)) : ids.indexOf(String(targetId)) + 1, 0, String(dragId)); diff --git a/public/js/modules/context-menu.js b/public/js/modules/context-menu.js index 6958fb9..6a8a39f 100644 --- a/public/js/modules/context-menu.js +++ b/public/js/modules/context-menu.js @@ -122,6 +122,25 @@ export function showContextMenu(x, y, card) { renderCtxTags(); tagInput.value = ''; tagInput.addEventListener('input', () => renderCtxTags(tagInput.value)); + tagInput.addEventListener('keydown', async e => { + if (e.key !== 'Enter') return; + e.preventDefault(); + const raw = tagInput.value.replace(/^#/, '').trim().toLowerCase(); + if (!raw) return; + const tag = raw; + if (!cardTags.includes(tag)) { + const newBody = (card.body || '').trimEnd() + ' #' + tag; + card.body = newBody; + cardTags.push(tag); + if (!allTags.includes(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(','); } + window._renderBoard(); + } + tagInput.value = ''; + renderCtxTags(''); + }); tags.classList.add('hidden'); menu.style.left = Math.min(x, innerWidth - 188) + 'px'; diff --git a/public/js/modules/detail.js b/public/js/modules/detail.js index afe0f54..6a2d789 100644 --- a/public/js/modules/detail.js +++ b/public/js/modules/detail.js @@ -613,20 +613,6 @@ document.addEventListener('keydown', e => { // Zen mode // Tree view toggle // Extract selection to new card -$('detail-extract')?.addEventListener('click', async () => { - const sel = window.getSelection(); - const text = sel?.toString(); - if (!text?.trim()) return; - await window._extractToCard(text); -}); - -$('detail-tree')?.addEventListener('click', () => { - const body = $('detail-body'); - if (!body) return; - body.classList.toggle('tree'); - $('detail-tree')?.classList.toggle('text-t-accent', body.classList.contains('tree')); - $('detail-tree')?.classList.toggle('border-t-accent', body.classList.contains('tree')); -}); $('detail-zen')?.addEventListener('click', () => { detail.classList.toggle('zen'); @@ -644,13 +630,29 @@ const splitSearch = $('split-search'); const splitPicks = $('split-picks'); const splitContent = $('split-content'); +function findTaggedCards(card) { + if (!card) return []; + const myTags = parseTags(card); + if (!myTags.length) return []; + return state.cards + .filter(c => c.id != card.id) + .map(c => { + const ct = parseTags(c); + const shared = myTags.filter(t => ct.some(ct2 => ct2 === t || ct2.startsWith(t + '/') || t.startsWith(ct2 + '/'))); + return shared.length ? { card: c, score: shared.length } : null; + }) + .filter(Boolean) + .sort((a, b) => b.score - a.score) + .slice(0, 12) + .map(s => s.card); +} + function findRelatedCards(card) { if (!card) return []; const id = card.id; const title = (card.title || '').toLowerCase(); const body = (card.body || '').toLowerCase(); - // Extract significant words (3+ chars) from title and body const stopwords = new Set(['the','and','for','that','this','with','from','are','was','were','been','have','has','had','not','but','what','all','can','her','his','how','its','may','new','now','old','see','way','who','did','get','let','say','she','too','use','eine','ein','der','die','das','und','ist','von','mit','den','dem','des','auf','fuer','auch','sich','noch','wie','man','aber','nach','bei','nur','dann','wenn','oder','als','bis','zum','zur','kann','doch','ich','wir','sie','aus','vor']); const words = `${card.title} ${stripMd(card.body)}`.toLowerCase() .split(/[^a-zA-Z0-9äöüß]+/) @@ -664,16 +666,11 @@ function findRelatedCards(card) { const ct = (c.title || '').toLowerCase(); const cb = stripMd(c.body || '').toLowerCase(); - // Backlink: this card references the other if (body.includes(`[[${c.title.toLowerCase()}]]`)) score += 80; - // Backlink: the other references this card if ((c.body || '').toLowerCase().includes(`[[${title}]]`)) score += 80; - // Same bucket if (card.bucket_id && c.bucket_id == card.bucket_id) score += 15; - // Same group if (card.parent_id && c.parent_id == card.parent_id) score += 20; - // Word overlap for (const w of wordSet) { if (ct.includes(w)) score += 6; if (cb.includes(w)) score += 2; @@ -687,19 +684,34 @@ function findRelatedCards(card) { .map(s => s.card); } -function renderSplitPicks(cards, label) { - if (!cards.length) { - splitPicks.innerHTML = `
no matches
`; - return; - } - splitPicks.innerHTML = - `
${label}
` + +function renderSplitGroup(cards, label) { + if (!cards.length) return ''; + return `
${label}
` + cards.map(c => `
${esc(c.title)} ${c.bucket_name ? `${esc(c.bucket_name)}` : ''}
`).join(''); +} +function renderSplitPicks(cards, label) { + if (!cards.length) { + splitPicks.innerHTML = `
no matches
`; + return; + } + splitPicks.innerHTML = renderSplitGroup(cards, label); + splitPicks.querySelectorAll('[data-split-card]').forEach(el => { + el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard)); + }); +} + +function renderSplitGroups(groups) { + const html = groups.filter(g => g.cards.length).map(g => renderSplitGroup(g.cards, g.label)).join(''); + if (!html) { + splitPicks.innerHTML = `
no matches
`; + return; + } + splitPicks.innerHTML = html; splitPicks.querySelectorAll('[data-split-card]').forEach(el => { el.addEventListener('click', () => loadSplitCard(el.dataset.splitCard)); }); @@ -768,8 +780,15 @@ function openSplitPane() { splitPane.classList.remove('hidden'); splitSearch.value = ''; splitContent.innerHTML = ''; + const tagged = findTaggedCards(state.viewingCard); const related = findRelatedCards(state.viewingCard); - renderSplitPicks(related, related.length ? 'related' : 'no related cards'); + // Dedupe: remove tagged cards from related + const taggedIds = new Set(tagged.map(c => c.id)); + const relatedOnly = related.filter(c => !taggedIds.has(c.id)); + renderSplitGroups([ + { label: 'tags', cards: tagged }, + { label: 'related', cards: relatedOnly }, + ]); splitSearch.focus(); } diff --git a/public/js/modules/editor.js b/public/js/modules/editor.js index db3dace..e91142c 100644 --- a/public/js/modules/editor.js +++ b/public/js/modules/editor.js @@ -294,6 +294,8 @@ window.addEventListener('resize', () => { if (linesVisible) updateGutter(); }); export function closeModal({ animate = true } = {}) { if (animate) startCloseMorph(); else clearMorphGhost(); + hideLinkAc(); + hideTagAc(); modal.classList.remove('open'); state.editingId = null; state.returnToGalleryAfterEdit = false; @@ -507,6 +509,13 @@ function showTagAc(query) { function hideTagAc() { tagAc.classList.add('hidden'); tagAcVisible = false; } +document.addEventListener('click', e => { + if (tagAcVisible && !tagAc.contains(e.target) && e.target !== body) hideTagAc(); +}); +document.addEventListener('keydown', e => { + if (tagAcVisible && e.key === 'Escape') { hideTagAc(); } +}); + function completeTagAc(idx) { const tag = tagAcItems[idx]; if (!tag) return;