diff --git a/packages/core/src/editor/managers/StyleManager.test.ts b/packages/core/src/editor/managers/StyleManager.test.ts new file mode 100644 index 0000000000..dc1b87ec90 --- /dev/null +++ b/packages/core/src/editor/managers/StyleManager.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { BlockNoteEditor } from "../BlockNoteEditor.js"; + +/** + * @vitest-environment jsdom + */ + +function createEditorWithLink() { + const editor = BlockNoteEditor.create(); + editor.mount(document.createElement("div")); + editor.replaceBlocks(editor.document, [ + { + id: "block", + type: "paragraph", + content: [ + { + type: "link", + href: "https://google.com", + content: [{ type: "text", text: "Google", styles: {} }], + }, + ], + }, + ]); + return editor; +} + +function linkContent(editor: BlockNoteEditor) { + const block = editor.getBlock("block")!; + return (block.content as any[]).map((inline) => ({ + type: inline.type, + href: inline.href, + text: inline.type === "link" ? inline.content[0].text : inline.text, + })); +} + +describe("StyleManager link editing at boundaries", () => { + it("edits the link when the caret is at its end", () => { + const editor = createEditorWithLink(); + editor.setTextCursorPosition("block", "end"); + + editor.editLink("https://google.com", "Google Search"); + + expect(linkContent(editor)).toEqual([ + { type: "link", href: "https://google.com", text: "Google Search" }, + ]); + }); + + it("deletes the link when the caret is at its end", () => { + const editor = createEditorWithLink(); + editor.setTextCursorPosition("block", "end"); + + editor.deleteLink(); + + expect(linkContent(editor)).toEqual([ + { type: "text", href: undefined, text: "Google" }, + ]); + }); + + it("edits the link when the caret is at its start", () => { + const editor = createEditorWithLink(); + editor.setTextCursorPosition("block", "start"); + + editor.editLink("https://google.com", "Google Search"); + + expect(linkContent(editor)).toEqual([ + { type: "link", href: "https://google.com", text: "Google Search" }, + ]); + }); + + it("does not throw for a position at the very end of the document", () => { + const editor = createEditorWithLink(); + const end = editor.prosemirrorState.doc.content.size; + + expect(() => + editor.editLink("https://github.com", "GitHub", end), + ).not.toThrow(); + expect(() => editor.deleteLink(end)).not.toThrow(); + }); +}); diff --git a/packages/core/src/editor/managers/StyleManager.ts b/packages/core/src/editor/managers/StyleManager.ts index 123ac6187b..7584040b1c 100644 --- a/packages/core/src/editor/managers/StyleManager.ts +++ b/packages/core/src/editor/managers/StyleManager.ts @@ -175,6 +175,25 @@ export class StyleManager< }); } + /** + * Find the link mark touching `position`: inside it, or at its start or end boundary, + * where the resolved position's marks alone would miss a non-inclusive link. + * Positions outside the document are skipped, so a caret at the very end never throws. + */ + private getLinkMarkAround(position: number) { + const size = this.editor.transact((tr) => tr.doc.content.size); + for (const pos of [position + 1, position, position - 1]) { + if (pos < 0 || pos > size) { + continue; + } + const linkData = this.getLinkMarkAtPos(pos); + if (linkData && position >= linkData.from && position <= linkData.to) { + return linkData; + } + } + return undefined; + } + /** * Gets the URL of the last link in the current selection, or `undefined` if there are no links in the selection. */ @@ -222,7 +241,7 @@ export class StyleManager< position = this.editor.transact((tr) => tr.selection.anchor), ) { this.editor.transact((tr) => { - const linkData = this.getLinkMarkAtPos(position + 1); + const linkData = this.getLinkMarkAround(position); const { from, to } = linkData || { from: tr.selection.from, to: tr.selection.to, @@ -246,7 +265,7 @@ export class StyleManager< position = this.editor.transact((tr) => tr.selection.anchor), ) { this.editor.transact((tr) => { - const linkData = this.getLinkMarkAtPos(position + 1); + const linkData = this.getLinkMarkAround(position); const { from, to } = linkData || { from: tr.selection.from, to: tr.selection.to,