tags
This commit is contained in:
1 parent
a9603aa0e8
commit
719f9d51ff
13 files changed
+625
-70
No files matched your search
+65
-11
@@ -1,7 +1,7 @@
|
||||
// ── Search ──
|
||||
|
||||
import { $, esc, escRe, fuzzyMatch, countSubstring, highlight } from './util.js';
|
||||
import { state } from './state.js';
|
||||
import { state, parseTags, tagChipHtml } from './state.js';
|
||||
import { renderMd, stripMd } from './markdown.js';
|
||||
|
||||
const overlay = $('search');
|
||||
@@ -50,6 +50,31 @@ function updatePreview() {
|
||||
for (const w of words) html = html.replace(new RegExp('(>)([^<]*?)(' + escRe(w) + ')', 'gi'), '$1$2<mark>$3</mark>');
|
||||
}
|
||||
$('search-preview-body').innerHTML = html;
|
||||
|
||||
const ptags = parseTags(card);
|
||||
const ptagEl = $('search-preview-tags');
|
||||
if (ptags.length) {
|
||||
ptagEl.classList.remove('hidden');
|
||||
ptagEl.innerHTML = ptags.map(t => tagChipHtml(t)).join('');
|
||||
ptagEl.querySelectorAll('.tag-seg[data-tag]').forEach(seg => {
|
||||
seg.addEventListener('click', e => {
|
||||
closeSearch();
|
||||
const tag = seg.dataset.tag;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const idx = state.activeTags.indexOf(tag);
|
||||
if (idx >= 0) state.activeTags.splice(idx, 1);
|
||||
else state.activeTags.push(tag);
|
||||
} else {
|
||||
state.activeTags = [tag];
|
||||
}
|
||||
window._renderTagBar?.();
|
||||
window._renderBoard();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ptagEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
$('search-preview-meta').textContent = '> created: ' + card.created_at + ' | modified: ' + card.updated_at + (card.bucket_name ? ' | bucket: ' + card.bucket_name : '');
|
||||
}
|
||||
|
||||
@@ -66,24 +91,51 @@ function renderHome() {
|
||||
function search(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
if (!q) { renderHome(); return; }
|
||||
const words = q.split(/\s+/).filter(Boolean);
|
||||
const scored = getPool().map(c => {
|
||||
|
||||
// Split into #tag filters and regular words
|
||||
const parts = q.split(/\s+/).filter(Boolean);
|
||||
const tagFilters = parts.filter(p => p.startsWith('#') && p.length > 1).map(p => p.substring(1));
|
||||
const words = parts.filter(p => !p.startsWith('#'));
|
||||
const textQ = words.join(' ');
|
||||
|
||||
// Pre-filter by tags (must match ALL)
|
||||
let pool = getPool();
|
||||
if (tagFilters.length) {
|
||||
pool = pool.filter(c => {
|
||||
const ct = (c.tags || '').toLowerCase().split(',');
|
||||
return tagFilters.every(tf => ct.some(t => t === tf || t.startsWith(tf + '/') || t.includes(tf)));
|
||||
});
|
||||
}
|
||||
|
||||
// Tag-only search (no words)
|
||||
if (!words.length) {
|
||||
hits = pool; active = pool.length ? 0 : -1;
|
||||
if (!pool.length) { results.innerHTML = `<div class="px-4 py-8 text-center"><i class="ph ph-hash text-2xl text-t-dim mb-2 block"></i><p class="text-xs text-t-dim font-pixel">no cards with ${tagFilters.map(t => '#' + t).join(' + ')}</p></div>`; footer.classList.add('hidden'); clearPreview(); return; }
|
||||
results.innerHTML = `<div class="px-4 pt-3 pb-1"><span class="font-pixel text-[9px] text-t-dim uppercase tracking-wider">${pool.length} tagged</span></div>` +
|
||||
pool.map((c, i) => hitHtml(c, i, null)).join('');
|
||||
footer.classList.remove('hidden'); bindHits(); updateActive(); updatePreview(); return;
|
||||
}
|
||||
|
||||
const scored = pool.map(c => {
|
||||
const title = (c.title || '').toLowerCase(), body = stripMd(c.body || '').toLowerCase();
|
||||
let score = 0, matched = false;
|
||||
if (title.includes(q)) { score += 150; matched = true; }
|
||||
if (body.includes(q)) { score += 40 + countSubstring(body, q) * 10; matched = true; }
|
||||
const tags = (c.tags || '').toLowerCase();
|
||||
let score = tagFilters.length ? 50 : 0, matched = tagFilters.length > 0;
|
||||
if (title.includes(textQ)) { score += 150; matched = true; }
|
||||
if (body.includes(textQ)) { score += 40 + countSubstring(body, textQ) * 10; matched = true; }
|
||||
if (tags.includes(textQ)) { score += 60; matched = true; }
|
||||
let allWords = true;
|
||||
for (const w of words) {
|
||||
const inT = title.includes(w), inB = body.includes(w);
|
||||
if (inT) { score += 30; if (new RegExp('(^|[\\s\\-_./])' + escRe(w)).test(title)) score += 20; if (title.startsWith(w)) score += 15; score += countSubstring(title, w) * 5; }
|
||||
if (inB) score += 8 + countSubstring(body, w) * 3;
|
||||
if (tags.includes(w)) score += 25;
|
||||
if (!inT && !inB) allWords = false;
|
||||
}
|
||||
if (allWords) { score += 50; matched = true; }
|
||||
const tf = fuzzyMatch(q, title), bf = fuzzyMatch(q, body);
|
||||
if (tf !== null) { score += 20 + tf * 2; matched = true; }
|
||||
if (bf !== null) { score += 5 + bf * 0.5; matched = true; }
|
||||
if (!matched) { let fh = 0; for (const w of words) { if (fuzzyMatch(w, title) !== null) { score += 10; fh++; } if (fuzzyMatch(w, body) !== null) { score += 3; fh++; } } if (fh > 0) matched = true; }
|
||||
if (!matched) {
|
||||
const tf = fuzzyMatch(textQ, title);
|
||||
if (tf !== null) { score += 15 + tf * 2; matched = true; }
|
||||
}
|
||||
if (!matched) return null;
|
||||
return { card: c, score: score - title.length * 0.1 };
|
||||
}).filter(Boolean).sort((a, b) => b.score - a.score);
|
||||
@@ -117,10 +169,12 @@ function hitHtml(card, idx, words) {
|
||||
snippet = highlight(esc(snippet), words);
|
||||
} else { snippet = esc(plain.substring(0, 120)) + (plain.length > 120 ? '...' : ''); }
|
||||
|
||||
const htags = parseTags(card);
|
||||
return `<div class="search-hit flex items-start gap-3 px-4 py-2.5 cursor-pointer transition-colors hover:bg-t-raised" data-idx="${idx}">
|
||||
<span class="w-2 h-2 rounded-full shrink-0 mt-1.5" style="background:${card.color}"></span>
|
||||
<div class="min-w-0 flex-1"><div class="text-xs text-t-bright font-pixel leading-relaxed truncate">${title}</div>
|
||||
<div class="text-[11px] text-t-muted leading-relaxed mt-0.5 line-clamp-2">${snippet}</div></div></div>`;
|
||||
<div class="text-[11px] text-t-muted leading-relaxed mt-0.5 line-clamp-2">${snippet}</div>
|
||||
${htags.length ? `<div class="flex flex-wrap gap-1 mt-1">${htags.slice(0, 3).map(t => `<span class="tag-chip" style="font-size:8px;padding:0 4px;">#${esc(t)}</span>`).join('')}</div>` : ''}</div></div>`;
|
||||
}
|
||||
|
||||
function bindHits() {
|
||||
|
||||
Reference in new issue
Block a user