diff --git a/patches/@blocknote__core@0.46.2.patch b/patches/@blocknote__core@0.46.2.patch index e666130d..1e199e11 100644 --- a/patches/@blocknote__core@0.46.2.patch +++ b/patches/@blocknote__core@0.46.2.patch @@ -117,6 +117,61 @@ } function kn(o, e) { const t = [ +--- a/src/extensions/SideMenu/SideMenu.ts ++++ b/src/extensions/SideMenu/SideMenu.ts +@@ -230,38 +230,49 @@ + // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block. + if ( + this.state?.show && + this.hoveredBlock?.hasAttribute("data-id") && + this.hoveredBlock?.getAttribute("data-id") === block.id + ) { + return; + } + + this.hoveredBlock = block.node; ++ const hoveredBlockId = this.hoveredBlock.getAttribute("data-id"); ++ const hoveredEditorBlock = hoveredBlockId ++ ? this.editor.getBlock(hoveredBlockId) ++ : undefined; ++ ++ if (!hoveredEditorBlock) { ++ if (this.state?.show) { ++ this.state.show = false; ++ this.updateState(this.state); ++ } ++ ++ return; ++ } + + // Shows or updates elements. + if (this.editor.isEditable) { + const blockContentBoundingBox = block.node.getBoundingClientRect(); + const column = block.node.closest("[data-node-type=column]"); + this.state = { + show: true, + referencePos: new DOMRect( + column + ? // We take the first child as column elements have some default + // padding. This is a little weird since this child element will + // be the first block, but since it's always non-nested and we + // only take the x coordinate, it's ok. + column.firstElementChild!.getBoundingClientRect().x + : ( + this.pmView.dom.firstChild as HTMLElement + ).getBoundingClientRect().x, + blockContentBoundingBox.y, + blockContentBoundingBox.width, + blockContentBoundingBox.height, + ), +- block: this.editor.getBlock( +- this.hoveredBlock!.getAttribute("data-id")!, +- )!, ++ block: hoveredEditorBlock, + }; + this.updateState(this.state); + } + }; --- a/src/extensions/SuggestionMenu/SuggestionMenu.ts +++ b/src/extensions/SuggestionMenu/SuggestionMenu.ts @@ -32,7 +32,7 @@ @@ -202,6 +257,48 @@ if ( --- a/dist/TrailingNode-CxM966vN.js +++ b/dist/TrailingNode-CxM966vN.js +@@ -1200,25 +1200,31 @@ + (r = this.state) != null && r.show && (this.state.show = !1, this.updateState(this.state)); + return; + } +- if (!((s = this.state) != null && s.show && ((i = this.hoveredBlock) != null && i.hasAttribute("data-id")) && ((a = this.hoveredBlock) == null ? void 0 : a.getAttribute("data-id")) === t.id) && (this.hoveredBlock = t.node, this.editor.isEditable)) { +- const c = t.node.getBoundingClientRect(), l = t.node.closest("[data-node-type=column]"); ++ if ((s = this.state) != null && s.show && ((i = this.hoveredBlock) != null && i.hasAttribute("data-id")) && ((a = this.hoveredBlock) == null ? void 0 : a.getAttribute("data-id")) === t.id) ++ return; ++ this.hoveredBlock = t.node; ++ const c = this.hoveredBlock.getAttribute("data-id"), l = c ? this.editor.getBlock(c) : void 0; ++ if (!l) { ++ (i = this.state) != null && i.show && (this.state.show = !1, this.updateState(this.state)); ++ return; ++ } ++ if (this.editor.isEditable) { ++ const u = t.node.getBoundingClientRect(), h = t.node.closest("[data-node-type=column]"); + this.state = { + show: !0, + referencePos: new DOMRect( +- l ? ( ++ h ? ( + // We take the first child as column elements have some default + // padding. This is a little weird since this child element will + // be the first block, but since it's always non-nested and we + // only take the x coordinate, it's ok. +- l.firstElementChild.getBoundingClientRect().x ++ h.firstElementChild.getBoundingClientRect().x + ) : this.pmView.dom.firstChild.getBoundingClientRect().x, +- c.y, +- c.width, +- c.height ++ u.y, ++ u.width, ++ u.height + ), +- block: this.editor.getBlock( +- this.hoveredBlock.getAttribute("data-id") +- ) ++ block: l + }, this.updateState(this.state); + } + }); @@ -1746,10 +1746,10 @@ ); this.state.rowIndex !== void 0 && this.state.colIndex !== void 0 && (this.state.rowIndex >= e && (this.state.rowIndex = e - 1), this.state.colIndex >= t && (this.state.colIndex = t - 1)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 100a2eaa..8306afe1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: '@blocknote/core@0.46.2': - hash: 28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42 + hash: 05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626 path: patches/@blocknote__core@0.46.2.patch '@blocknote/react@0.46.2': hash: e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76 @@ -27,10 +27,10 @@ importers: version: 0.78.0(zod@4.3.6) '@blocknote/code-block': specifier: ^0.46.2 - version: 0.46.2(@blocknote/core@0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)) + version: 0.46.2(@blocknote/core@0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)) '@blocknote/core': specifier: ^0.46.2 - version: 0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + version: 0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@blocknote/mantine': specifier: ^0.46.2 version: 0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -5359,9 +5359,9 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))': + '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))': dependencies: - '@blocknote/core': 0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@shikijs/core': 3.23.0 '@shikijs/engine-javascript': 3.23.0 '@shikijs/langs': 3.23.0 @@ -5369,7 +5369,7 @@ snapshots: '@shikijs/themes': 3.23.0 '@shikijs/types': 3.22.0 - '@blocknote/core@0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@blocknote/core@0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)': dependencies: '@emoji-mart/data': 1.2.1 '@handlewithcare/prosemirror-inputrules': 0.1.4(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) @@ -5421,7 +5421,7 @@ snapshots: '@blocknote/mantine@0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@blocknote/core': 0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@blocknote/react': 0.46.2(patch_hash=e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/core': 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/hooks': 8.3.14(react@19.2.4) @@ -5443,7 +5443,7 @@ snapshots: '@blocknote/react@0.46.2(patch_hash=e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@blocknote/core': 0.46.2(patch_hash=28d8e7bb45cd7d97aff60af3926d65b831605bfe4d582a1da725d214d3fb1f42)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=05f1896ec4fac0d7ffd1f823acd036baa82c1d6170a8b5699ddeb6a4dd2b3626)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@emoji-mart/data': 1.2.1 '@floating-ui/react': 0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@floating-ui/utils': 0.2.10 diff --git a/src/lib/blockNoteSideMenu.regression.test.ts b/src/lib/blockNoteSideMenu.regression.test.ts new file mode 100644 index 00000000..6a340b45 --- /dev/null +++ b/src/lib/blockNoteSideMenu.regression.test.ts @@ -0,0 +1,72 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { SideMenuView } from '../../node_modules/@blocknote/core/src/extensions/SideMenu/SideMenu' + +const originalElementsFromPoint = document.elementsFromPoint + +function createRect({ left, top, width, height }: { left: number; top: number; width: number; height: number }) { + return { + left, + top, + right: left + width, + bottom: top + height, + width, + height, + x: left, + y: top, + toJSON: () => ({}), + } as DOMRect +} + +function createStaleEditorDom() { + const editor = document.createElement('div') + editor.className = 'bn-editor' + editor.getBoundingClientRect = vi.fn(() => createRect({ left: 0, top: 0, width: 480, height: 240 })) + + const blockGroup = document.createElement('div') + blockGroup.className = 'bn-block-group' + blockGroup.getBoundingClientRect = vi.fn(() => createRect({ left: 20, top: 20, width: 360, height: 160 })) + + const block = document.createElement('div') + block.setAttribute('data-node-type', 'blockContainer') + block.setAttribute('data-id', 'stale-table-block') + block.getBoundingClientRect = vi.fn(() => createRect({ left: 20, top: 40, width: 260, height: 48 })) + + blockGroup.appendChild(block) + editor.appendChild(blockGroup) + document.body.appendChild(editor) + + return { block, editor } +} + +describe('patched BlockNote side menu lifecycle', () => { + afterEach(() => { + document.elementsFromPoint = originalElementsFromPoint + document.body.innerHTML = '' + }) + + it('does not publish side-menu state for a stale hovered block id', () => { + const { block, editor } = createStaleEditorDom() + document.elementsFromPoint = vi.fn(() => [block, editor]) + + const updates: unknown[] = [] + const view = new SideMenuView( + { + getBlock: vi.fn(() => undefined), + isEditable: true, + } as never, + { + dom: editor, + root: document, + } as never, + (state) => updates.push(state), + ) + + try { + view.onMouseMove(new MouseEvent('mousemove', { clientX: 40, clientY: 50 })) + + expect(updates).toEqual([]) + } finally { + view.destroy() + } + }) +})