tags
This commit is contained in:
1 parent
a9603aa0e8
commit
719f9d51ff
13 files changed
+625
-70
No files matched your search
@@ -16,8 +16,10 @@ const inpColor = $('inp-color');
|
||||
const inpBucket = $('inp-bucket');
|
||||
const presets = $('color-presets');
|
||||
const linkAc = $('link-ac');
|
||||
const tagAc = $('tag-ac');
|
||||
|
||||
let linkAcVisible = false, linkAcItems = [], linkAcActive = 0;
|
||||
let tagAcVisible = false, tagAcItems = [], tagAcActive = 0;
|
||||
let morphGhost = null, morphTimer = null;
|
||||
|
||||
function clearMorphGhost() {
|
||||
@@ -112,6 +114,8 @@ CARD_COLORS.forEach(c => {
|
||||
|
||||
function selectColor(hex) {
|
||||
inpColor.value = hex;
|
||||
const bar = $('editor-color');
|
||||
if (bar) bar.style.background = hex;
|
||||
presets.querySelectorAll('.color-swatch').forEach(el => {
|
||||
const a = el.dataset.color === hex;
|
||||
el.classList.toggle('border-t-bright', a);
|
||||
@@ -450,11 +454,69 @@ function completeLinkAc(idx) {
|
||||
}
|
||||
|
||||
body.addEventListener('input', () => {
|
||||
const info = getLinkQuery();
|
||||
info ? showLinkAc(info.query) : hideLinkAc();
|
||||
const linkInfo = getLinkQuery();
|
||||
linkInfo ? showLinkAc(linkInfo.query) : hideLinkAc();
|
||||
const tagInfo = getTagQuery();
|
||||
tagInfo ? showTagAc(tagInfo.query) : hideTagAc();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// ── Tag autocomplete (#) ──
|
||||
|
||||
function getTagQuery() {
|
||||
const before = body.value.substring(0, body.selectionStart);
|
||||
// Find # preceded by whitespace or start of line, not part of heading (# + space)
|
||||
const m = before.match(/(?:^|[\s])#([a-zA-Z0-9_\/-]*)$/m);
|
||||
if (!m) return null;
|
||||
const start = before.length - m[0].length + (m[0].startsWith('#') ? 0 : 1); // skip leading whitespace
|
||||
return { start, query: m[1] };
|
||||
}
|
||||
|
||||
function getAllTags() {
|
||||
const set = new Set();
|
||||
for (const c of state.cards) {
|
||||
for (const t of (c.tags || '').split(',')) { if (t) set.add(t); }
|
||||
}
|
||||
return [...set].sort();
|
||||
}
|
||||
|
||||
function showTagAc(query) {
|
||||
const q = query.toLowerCase();
|
||||
const all = getAllTags();
|
||||
tagAcItems = all.filter(t => !q || t.includes(q));
|
||||
if (!tagAcItems.length) { hideTagAc(); return; }
|
||||
tagAcActive = 0;
|
||||
tagAc.innerHTML = tagAcItems.slice(0, 8).map((t, i) => `
|
||||
<div class="ac-item ${i === 0 ? 'active' : ''}" data-idx="${i}">
|
||||
<span class="ac-title font-pixel">#${esc(t)}</span>
|
||||
</div>`).join('');
|
||||
tagAcItems = tagAcItems.slice(0, 8);
|
||||
tagAc.querySelectorAll('.ac-item').forEach(el => {
|
||||
el.addEventListener('mousedown', e => { e.preventDefault(); completeTagAc(parseInt(el.dataset.idx)); });
|
||||
});
|
||||
const rect = body.getBoundingClientRect();
|
||||
const lines = body.value.substring(0, body.selectionStart).split('\n');
|
||||
tagAc.style.top = Math.min(rect.top + (lines.length - 1) * 22 - body.scrollTop + 26, innerHeight - 200) + 'px';
|
||||
tagAc.style.left = Math.min(rect.left + 40, innerWidth - 320) + 'px';
|
||||
tagAc.classList.remove('hidden');
|
||||
tagAcVisible = true;
|
||||
}
|
||||
|
||||
function hideTagAc() { tagAc.classList.add('hidden'); tagAcVisible = false; }
|
||||
|
||||
function completeTagAc(idx) {
|
||||
const tag = tagAcItems[idx];
|
||||
if (!tag) return;
|
||||
const info = getTagQuery();
|
||||
if (!info) { hideTagAc(); return; }
|
||||
const before = body.value.substring(0, info.start);
|
||||
const after = body.value.substring(body.selectionStart);
|
||||
const ins = `#${tag} `;
|
||||
body.value = before + ins + after;
|
||||
body.selectionStart = body.selectionEnd = info.start + ins.length;
|
||||
hideTagAc(); updatePreview(); body.focus();
|
||||
}
|
||||
|
||||
// ── Line helpers ──
|
||||
|
||||
function getLineInfo() {
|
||||
@@ -511,6 +573,17 @@ body.addEventListener('keydown', e => {
|
||||
linkAc.querySelectorAll('.ac-item').forEach((el, i) => el.classList.toggle('active', i === linkAcActive));
|
||||
return;
|
||||
}
|
||||
if (tagAcVisible) {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); tagAcActive = Math.min(tagAcActive + 1, tagAcItems.length - 1); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); tagAcActive = Math.max(tagAcActive - 1, 0); }
|
||||
else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); completeTagAc(tagAcActive); return; }
|
||||
else if (e.key === 'Escape') { e.preventDefault(); hideTagAc(); return; }
|
||||
tagAc.querySelectorAll('.ac-item').forEach((el, i) => {
|
||||
el.classList.toggle('active', i === tagAcActive);
|
||||
if (i === tagAcActive) el.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Alt+Arrow: move line
|
||||
if (e.altKey && !e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
||||
e.preventDefault();
|
||||
@@ -526,7 +599,7 @@ body.addEventListener('keydown', e => {
|
||||
if (e.key === 'Tab') { e.preventDefault(); insertMd(' '); }
|
||||
});
|
||||
|
||||
body.addEventListener('blur', () => setTimeout(hideLinkAc, 150));
|
||||
body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hideTagAc, 150); });
|
||||
|
||||
// ── File upload (paste + drag-drop) ──
|
||||
|
||||
|
||||
Reference in new issue
Block a user