fix: @-mention autocomplete — Enter/Tab/click now insert correctly
The old approach used document.execCommand("insertText") to directly
manipulate the contentEditable DOM, but MDXEditor (Lexical) reverted
these changes during reconciliation causing the "blink" bug.
Fix: work at the markdown string level instead — find the @query in
the markdown, replace it with @Name, and update via setMarkdown().
Also add an input event listener alongside selectionchange for more
reliable mention detection (fixes space-to-dismiss).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -108,17 +108,13 @@ function detectMention(container: HTMLElement): MentionState | null {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertMention(state: MentionState, option: MentionOption) {
|
/** Replace `@<query>` in the markdown string with `@<Name> `. */
|
||||||
const range = document.createRange();
|
function applyMention(markdown: string, query: string, option: MentionOption): string {
|
||||||
range.setStart(state.textNode, state.atPos);
|
const search = `@${query}`;
|
||||||
range.setEnd(state.textNode, state.endPos);
|
const replacement = `@${option.name} `;
|
||||||
|
const idx = markdown.lastIndexOf(search);
|
||||||
const sel = window.getSelection();
|
if (idx === -1) return markdown;
|
||||||
sel?.removeAllRanges();
|
return markdown.slice(0, idx) + replacement + markdown.slice(idx + search.length);
|
||||||
sel?.addRange(range);
|
|
||||||
|
|
||||||
// insertText preserves undo stack and triggers editor onChange
|
|
||||||
document.execCommand("insertText", false, `@${option.name} `);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Component ---- */
|
/* ---- Component ---- */
|
||||||
@@ -213,17 +209,35 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mentions || mentions.length === 0) return;
|
if (!mentions || mentions.length === 0) return;
|
||||||
|
|
||||||
|
const el = containerRef.current;
|
||||||
|
// Listen for input events on the container so mention detection
|
||||||
|
// also fires after typing (e.g. space to dismiss).
|
||||||
|
const onInput = () => requestAnimationFrame(checkMention);
|
||||||
|
|
||||||
document.addEventListener("selectionchange", checkMention);
|
document.addEventListener("selectionchange", checkMention);
|
||||||
return () => document.removeEventListener("selectionchange", checkMention);
|
el?.addEventListener("input", onInput, true);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("selectionchange", checkMention);
|
||||||
|
el?.removeEventListener("input", onInput, true);
|
||||||
|
};
|
||||||
}, [checkMention, mentions]);
|
}, [checkMention, mentions]);
|
||||||
|
|
||||||
const selectMention = useCallback(
|
const selectMention = useCallback(
|
||||||
(option: MentionOption) => {
|
(option: MentionOption) => {
|
||||||
if (!mentionState) return;
|
if (!mentionState) return;
|
||||||
insertMention(mentionState, option);
|
const current = latestValueRef.current;
|
||||||
|
const next = applyMention(current, mentionState.query, option);
|
||||||
|
if (next !== current) {
|
||||||
|
latestValueRef.current = next;
|
||||||
|
ref.current?.setMarkdown(next);
|
||||||
|
onChange(next);
|
||||||
|
}
|
||||||
setMentionState(null);
|
setMentionState(null);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[mentionState],
|
[mentionState, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
function hasFilePayload(evt: DragEvent<HTMLDivElement>) {
|
function hasFilePayload(evt: DragEvent<HTMLDivElement>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user