mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 14:43:06 +08:00
f5d794220e
* 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
104 lines
2.5 KiB
TypeScript
104 lines
2.5 KiB
TypeScript
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
|
import React, { useCallback, useState } from "react";
|
|
import { TextSelection } from "@tiptap/pm/state";
|
|
import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts";
|
|
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
|
import { LinkPreviewPanel } from "@/features/editor/components/link/link-preview.tsx";
|
|
import { Card } from "@mantine/core";
|
|
import { useEditorState } from "@tiptap/react";
|
|
|
|
export function LinkMenu({ editor, appendTo }: EditorMenuProps) {
|
|
const [showEdit, setShowEdit] = useState(false);
|
|
|
|
const shouldShow = useCallback(() => {
|
|
return editor.isActive("link");
|
|
}, [editor]);
|
|
|
|
const editorState = useEditorState({
|
|
editor,
|
|
selector: (ctx) => {
|
|
if (!ctx.editor) {
|
|
return null;
|
|
}
|
|
const link = ctx.editor.getAttributes("link");
|
|
return {
|
|
href: link.href,
|
|
};
|
|
},
|
|
});
|
|
|
|
const handleEdit = useCallback(() => {
|
|
setShowEdit(true);
|
|
}, []);
|
|
|
|
const onSetLink = useCallback(
|
|
(url: string) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.extendMarkRange("link")
|
|
.setLink({ href: url })
|
|
.command(({ tr }) => {
|
|
tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
|
|
return true;
|
|
})
|
|
.run();
|
|
setShowEdit(false);
|
|
},
|
|
[editor],
|
|
);
|
|
|
|
const onUnsetLink = useCallback(() => {
|
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
setShowEdit(false);
|
|
return null;
|
|
}, [editor]);
|
|
|
|
const onShowEdit = useCallback(() => {
|
|
setShowEdit(true);
|
|
}, []);
|
|
|
|
const onHideEdit = useCallback(() => {
|
|
setShowEdit(false);
|
|
}, []);
|
|
|
|
return (
|
|
<BaseBubbleMenu
|
|
editor={editor}
|
|
pluginKey={`link-menu`}
|
|
updateDelay={0}
|
|
options={{
|
|
onHide: () => {
|
|
setShowEdit(false);
|
|
},
|
|
placement: "bottom",
|
|
offset: 5,
|
|
// zIndex: 101,
|
|
}}
|
|
shouldShow={shouldShow}
|
|
>
|
|
{showEdit ? (
|
|
<Card
|
|
withBorder
|
|
radius="md"
|
|
padding="xs"
|
|
bg="var(--mantine-color-body)"
|
|
>
|
|
<LinkEditorPanel
|
|
initialUrl={editorState?.href}
|
|
onSetLink={onSetLink}
|
|
/>
|
|
</Card>
|
|
) : (
|
|
<LinkPreviewPanel
|
|
url={editorState?.href}
|
|
onClear={onUnsetLink}
|
|
onEdit={handleEdit}
|
|
/>
|
|
)}
|
|
</BaseBubbleMenu>
|
|
);
|
|
}
|
|
|
|
export default LinkMenu;
|