mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
fix: resolve keystroke input being swallowed after link in Firefox (#1922)
* fix: resolve keystroke input being swallowed after link in Firefox In Firefox, when the cursor is at the right boundary of a link mark, contenteditable inserts new text inside the <a> element. ProseMirror then rejects the DOM mutation because the link mark has inclusive: false, causing keystrokes to be silently swallowed. Unlike Chrome, Firefox also does not fire ProseMirror's handleTextInput callback in this state. This adds a ProseMirror plugin that intercepts printable character keydowns at link mark boundaries and programmatically inserts the text without the link mark, bypassing Firefox's native contenteditable behavior entirely. Fixes #1773 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve keystroke input being swallowed before a link in Firefox Extend the linkBoundaryInput plugin to also handle the left boundary of links, where the cursor is just before a link (e.g. at the start of a line). Firefox inserts text inside the <a> element in this case too, causing ProseMirror to reject the mutation. Fixes #1748
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
|
||||
export const LinkExtension = TiptapLink.extend({
|
||||
@@ -66,6 +66,55 @@ export const LinkExtension = TiptapLink.extend({
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Fix for Firefox: when the cursor is at a boundary of a link,
|
||||
// Firefox's contenteditable inserts new text *inside* the <a> element.
|
||||
// ProseMirror then rejects the mutation because inclusive is false,
|
||||
// causing keystrokes to be silently swallowed. Firefox also does not
|
||||
// fire handleTextInput in this state, so we intercept at handleKeyDown.
|
||||
// This handles both:
|
||||
// - right boundary: cursor just after a link (typing appends to link)
|
||||
// - left boundary: cursor just before a link, e.g. at the start of a
|
||||
// line (#1748), where Firefox places new text inside the link node
|
||||
new Plugin({
|
||||
key: new PluginKey("linkBoundaryInput"),
|
||||
props: {
|
||||
handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
|
||||
// Only handle single printable characters
|
||||
if (event.key.length !== 1) return false;
|
||||
// Don't handle modified keys (shortcuts) or composing (IME)
|
||||
if (event.ctrlKey || event.metaKey || event.altKey || event.isComposing) return false;
|
||||
|
||||
const { state } = view;
|
||||
const linkType = state.schema.marks.link;
|
||||
if (!linkType) return false;
|
||||
|
||||
// Don't interfere if the user has explicitly set storedMarks
|
||||
if (state.storedMarks !== null) return false;
|
||||
|
||||
const { from, to } = state.selection;
|
||||
const $from = state.doc.resolve(from);
|
||||
const nodeBefore = $from.nodeBefore;
|
||||
const nodeAfter = $from.nodeAfter;
|
||||
|
||||
const linkBefore = nodeBefore && linkType.isInSet(nodeBefore.marks);
|
||||
const linkAfter = nodeAfter && linkType.isInSet(nodeAfter.marks);
|
||||
|
||||
// If both sides have link marks we're in the middle — don't interfere
|
||||
if (linkBefore && linkAfter) return false;
|
||||
|
||||
// Not at any link boundary — nothing to do
|
||||
if (!linkBefore && !linkAfter) return false;
|
||||
|
||||
// We're at a link boundary (left or right).
|
||||
// Prevent native input and insert text without the link mark.
|
||||
event.preventDefault();
|
||||
const tr = state.tr.insertText(event.key, from, to);
|
||||
tr.removeMark(from, from + event.key.length, linkType);
|
||||
view.dispatch(tr.scrollIntoView());
|
||||
return true;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user