From fa4d3046d0f67598db2094574c3936eaf573179c Mon Sep 17 00:00:00 2001 From: beo3000 Date: Fri, 20 Feb 2026 17:41:53 +0100 Subject: [PATCH] fix mention problem --- ka-note/client/src/lib/actions/mention.ts | 32 ++++++++++++++----- .../client/src/lib/editor/tiptapMention.ts | 21 +++++++++--- ka-note/client/src/lib/utils/extractors.ts | 6 ++-- .../client/src/lib/utils/renderMarkdown.ts | 14 ++++---- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/ka-note/client/src/lib/actions/mention.ts b/ka-note/client/src/lib/actions/mention.ts index 50ff46f..0c14051 100644 --- a/ka-note/client/src/lib/actions/mention.ts +++ b/ka-note/client/src/lib/actions/mention.ts @@ -71,30 +71,36 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) { }); } - function insertText(text: string) { - const before = node.value.slice(0, mentionStart); - const after = node.value.slice(node.selectionStart ?? 0); + function insertTextAt(from: number, to: number, text: string) { + const before = node.value.slice(0, from); + const after = node.value.slice(to); node.value = before + text + ' ' + after; - const newPos = mentionStart + text.length + 1; + const newPos = from + text.length + 1; node.selectionStart = newPos; node.selectionEnd = newPos; node.dispatchEvent(new Event('input', { bubbles: true })); } async function selectItem(index: number) { - console.log('[mention-action] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'mentionStart:', mentionStart, 'value:', JSON.stringify(node.value)); + // Save positions before any async work (hide/blur could reset mentionStart) + const savedFrom = mentionStart; + const savedTo = node.selectionStart ?? node.value.length; + console.log('[mention-action] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'from:', savedFrom, 'to:', savedTo); suppressInput = true; try { if (index < items.length) { - console.log('[mention-action] inserting:', items[index].insertText); - insertText(items[index].insertText); + const text = items[index].insertText; + console.log('[mention-action] inserting:', text); + insertTextAt(savedFrom, savedTo, text); console.log('[mention-action] after insert, value:', JSON.stringify(node.value)); } else { const opt = createOptions[index - items.length]; if (!opt) return; await createMentionContext(opt); const tag = opt.type === 'company' ? quoteMention('@F:', opt.query) : opt.type === 'person' ? quoteMention('@', opt.query) : quoteMention('@P:', opt.query); - insertText(tag); + console.log('[mention-action] inserting after create:', tag); + insertTextAt(savedFrom, savedTo, tag); + console.log('[mention-action] after create insert, value:', JSON.stringify(node.value)); } } catch (err) { console.error('mention selectItem error:', err); @@ -112,6 +118,15 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) { return; } const result = await fetchMentionItems(query); + + // Re-check after async: state may have changed (e.g. selectItem ran) + if (suppressInput) return; + const queryNow = getQuery(); + if (queryNow === null) { + if (active) hide(); + return; + } + items = result.items; createOptions = result.createOptions; selectedIndex = 0; @@ -144,6 +159,7 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) { } function handleBlur() { + if (suppressInput) return; setTimeout(hide, 150); } diff --git a/ka-note/client/src/lib/editor/tiptapMention.ts b/ka-note/client/src/lib/editor/tiptapMention.ts index 7915bc4..d9a48d8 100644 --- a/ka-note/client/src/lib/editor/tiptapMention.ts +++ b/ka-note/client/src/lib/editor/tiptapMention.ts @@ -76,13 +76,16 @@ export const TiptapMention = Extension.create({ } async function selectItem(index: number) { - console.log('[tiptap-mention] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'mentionFrom:', mentionFrom); + // Save positions before any async work (hide could reset mentionFrom) + const savedFrom = mentionFrom; + const savedTo = editor.state.selection.from; + console.log('[tiptap-mention] selectItem', index, 'items:', items.length, 'create:', createOptions.length, 'from:', savedFrom, 'to:', savedTo); try { if (index < items.length) { const text = items[index].insertText; - console.log('[tiptap-mention] inserting:', text, 'deleteRange:', mentionFrom, '->', editor.state.selection.from); + console.log('[tiptap-mention] inserting:', text); editor.chain().focus() - .deleteRange({ from: mentionFrom, to: editor.state.selection.from }) + .deleteRange({ from: savedFrom, to: savedTo }) .insertContent(text + ' ') .run(); console.log('[tiptap-mention] after insert, content:', editor.storage.markdown.getMarkdown().slice(0, 100)); @@ -91,8 +94,9 @@ export const TiptapMention = Extension.create({ if (!opt) return; await createMentionContext(opt); const tag = opt.type === 'company' ? quoteMention('@F:', opt.query) : opt.type === 'person' ? quoteMention('@', opt.query) : quoteMention('@P:', opt.query); + console.log('[tiptap-mention] inserting after create:', tag); editor.chain().focus() - .deleteRange({ from: mentionFrom, to: editor.state.selection.from }) + .deleteRange({ from: savedFrom, to: savedTo }) .insertContent(tag + ' ') .run(); } @@ -119,7 +123,7 @@ export const TiptapMention = Extension.create({ if (i === 0 || /\s/.test(textBefore[i - 1])) { const query = textBefore.slice(i + 1); // Skip @P: and @F: prefixes (handled differently) - if (query.startsWith('P:') || query.startsWith('F:')) return null; + if (/^[Pp]:|^[Ff]:/.test(query)) return null; const docFrom = from - (textBefore.length - i); return { active: true, query, from: docFrom }; } @@ -139,6 +143,13 @@ export const TiptapMention = Extension.create({ mentionFrom = mentionState.from; const result = await fetchMentionItems(mentionState.query); + + // Re-check after async: state may have changed (e.g. selectItem ran) + if (!getQueryFromState(view)) { + if (active) hide(); + return; + } + items = result.items; createOptions = result.createOptions; selectedIndex = 0; diff --git a/ka-note/client/src/lib/utils/extractors.ts b/ka-note/client/src/lib/utils/extractors.ts index cc18a65..e0df826 100644 --- a/ka-note/client/src/lib/utils/extractors.ts +++ b/ka-note/client/src/lib/utils/extractors.ts @@ -9,7 +9,7 @@ export function extractAssignments(text: string): string[] { } export function extractProjects(text: string): string[] { - const regex = /@P:(?:"([^"]+)"|([\w-]+))/g; + const regex = /@[Pp]:(?:"([^"]+)"|([\w-]+))/g; const result: string[] = []; let match; while ((match = regex.exec(text)) !== null) { @@ -19,7 +19,7 @@ export function extractProjects(text: string): string[] { } export function extractMentions(text: string): string[] { - const regex = /@(?!P:|F:)(?:"([^"]+)"|([\w-]+))/g; + const regex = /@(?![Pp]:|[Ff]:)(?:"([^"]+)"|([\w-]+))/g; const result: string[] = []; let match; while ((match = regex.exec(text)) !== null) { @@ -29,7 +29,7 @@ export function extractMentions(text: string): string[] { } export function extractCompanies(text: string): string[] { - const regex = /@F:(?:"([^"]+)"|([\w-]+))/g; + const regex = /@[Ff]:(?:"([^"]+)"|([\w-]+))/g; const result: string[] = []; let match; while ((match = regex.exec(text)) !== null) { diff --git a/ka-note/client/src/lib/utils/renderMarkdown.ts b/ka-note/client/src/lib/utils/renderMarkdown.ts index 8055704..eb3203f 100644 --- a/ka-note/client/src/lib/utils/renderMarkdown.ts +++ b/ka-note/client/src/lib/utils/renderMarkdown.ts @@ -22,9 +22,9 @@ const assignmentExt: TokenizerExtension & RendererExtension = { const projectRefExt: TokenizerExtension & RendererExtension = { name: 'projectRef', level: 'inline', - start(src: string) { return src.indexOf('@P:'); }, + start(src: string) { return src.search(/@[Pp]:/); }, tokenizer(src: string) { - const match = /^@P:"(.+?)"/.exec(src) ?? /^@P:([\w-]+)/.exec(src); + const match = /^@[Pp]:"(.+?)"/.exec(src) ?? /^@[Pp]:([\w-]+)/.exec(src); if (match) { return { type: 'projectRef', raw: match[0], project: match[1] }; } @@ -37,9 +37,9 @@ const projectRefExt: TokenizerExtension & RendererExtension = { const companyRefExt: TokenizerExtension & RendererExtension = { name: 'companyRef', level: 'inline', - start(src: string) { return src.indexOf('@F:'); }, + start(src: string) { return src.search(/@[Ff]:/); }, tokenizer(src: string) { - const match = /^@F:"(.+?)"/.exec(src) ?? /^@F:([\w-]+)/.exec(src); + const match = /^@[Ff]:"(.+?)"/.exec(src) ?? /^@[Ff]:([\w-]+)/.exec(src); if (match) { return { type: 'companyRef', raw: match[0], company: match[1] }; } @@ -55,13 +55,13 @@ const personMentionExt: TokenizerExtension & RendererExtension = { start(src: string) { const idx = src.indexOf('@'); if (idx === -1) return -1; - // Skip if followed by P: or F: (handled by other extensions) + // Skip if followed by P:/p: or F:/f: (handled by other extensions) const after = src.slice(idx + 1); - if (after.startsWith('P:') || after.startsWith('F:')) return src.indexOf('@', idx + 1); + if (/^[Pp]:|^[Ff]:/.test(after)) return src.indexOf('@', idx + 1); return idx; }, tokenizer(src: string) { - const match = /^@(?!P:|F:)"(.+?)"/.exec(src) ?? /^@(?!P:|F:)([\w-]+)/.exec(src); + const match = /^@(?![Pp]:|[Ff]:)"(.+?)"/.exec(src) ?? /^@(?![Pp]:|[Ff]:)([\w-]+)/.exec(src); if (match) { return { type: 'personMention', raw: match[0], person: match[1] }; }