mirror of
https://github.com/docmost/docmost.git
synced 2026-05-14 12:44:16 +08:00
d6068310b4
Refactor link.ts to simplify HTML parsing and rendering logic.
97 lines
3.4 KiB
TypeScript
97 lines
3.4 KiB
TypeScript
import TiptapLink from '@tiptap/extension-link';
|
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
import { EditorView } from '@tiptap/pm/view';
|
|
|
|
export const LinkExtension = TiptapLink.extend({
|
|
inclusive: false,
|
|
|
|
addAttributes() {
|
|
return {
|
|
...this.parent?.(),
|
|
internal: {
|
|
default: false,
|
|
parseHTML: (element: HTMLElement) =>
|
|
element.getAttribute('data-internal') === 'true',
|
|
renderHTML: (attributes) =>
|
|
attributes.internal ? { 'data-internal': 'true' } : {},
|
|
},
|
|
};
|
|
},
|
|
|
|
addProseMirrorPlugins() {
|
|
const { editor } = this;
|
|
|
|
return [
|
|
...(this.parent?.() || []),
|
|
new Plugin({
|
|
props: {
|
|
handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
|
|
const { selection } = editor.state;
|
|
|
|
if (event.key === 'Escape' && selection.empty !== true) {
|
|
editor.commands.focus(selection.to, { scrollIntoView: false });
|
|
}
|
|
|
|
return false;
|
|
},
|
|
},
|
|
}),
|
|
// 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;
|
|
},
|
|
},
|
|
}),
|
|
];
|
|
},
|
|
});
|