From 63211b2ed2ef4ab14edc4261e5461988966adc15 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Fri, 3 Oct 2025 13:00:46 +0200 Subject: [PATCH 1/2] fix: allow listening to `onChange` and other events before the underlying editor is initialized --- docs/content/docs/reference/editor/events.mdx | 22 ++++- .../core/src/editor/BlockNoteEditor.test.ts | 40 +++++++++ packages/core/src/editor/BlockNoteEditor.ts | 40 ++++++++- .../core/src/editor/managers/EventManager.ts | 88 +++++++++++-------- 4 files changed, 153 insertions(+), 37 deletions(-) diff --git a/docs/content/docs/reference/editor/events.mdx b/docs/content/docs/reference/editor/events.mdx index cde3d90456..f8cd85822a 100644 --- a/docs/content/docs/reference/editor/events.mdx +++ b/docs/content/docs/reference/editor/events.mdx @@ -12,7 +12,7 @@ BlockNote provides several event callbacks that allow you to respond to changes The editor emits events for: -- **Editor initialization** - When the editor is ready for use +- **Editor lifecycle** - When the editor is mounted, unmounted, etc. - **Content changes** - When blocks are inserted, updated, or deleted - **Selection changes** - When the cursor position or selection changes @@ -27,6 +27,26 @@ editor.onCreate(() => { }); ``` +## `onMount` + +The `onMount` callback is called when the editor has been mounted. + +```typescript +editor.onMount(() => { + console.log("Editor is mounted"); +}); +``` + +## `onUnmount` + +The `onUnmount` callback is called when the editor has been unmounted. + +```typescript +editor.onUnmount(() => { + console.log("Editor is unmounted"); +}); +``` + ## `onSelectionChange` The `onSelectionChange` callback is called whenever the editor's selection changes, including cursor movements and text selections. diff --git a/packages/core/src/editor/BlockNoteEditor.test.ts b/packages/core/src/editor/BlockNoteEditor.test.ts index 7b109977e2..4cfd7cf062 100644 --- a/packages/core/src/editor/BlockNoteEditor.test.ts +++ b/packages/core/src/editor/BlockNoteEditor.test.ts @@ -4,6 +4,7 @@ import { getNearestBlockPos, } from "../api/getBlockInfoFromPos.js"; import { BlockNoteEditor } from "./BlockNoteEditor.js"; +import { BlockNoteExtension } from "./BlockNoteExtension.js"; /** * @vitest-environment jsdom @@ -102,3 +103,42 @@ it("block prop types", () => { expect(level).toBe(1); } }); + +it("onMount and onUnmount", () => { + const editor = BlockNoteEditor.create(); + let mounted = false; + let unmounted = false; + editor.onMount(() => { + mounted = true; + }); + editor.onUnmount(() => { + unmounted = true; + }); + editor.mount(document.createElement("div")); + expect(mounted).toBe(true); + expect(unmounted).toBe(false); + editor.unmount(); + expect(mounted).toBe(true); + expect(unmounted).toBe(true); +}); + +it("onCreate event", () => { + let created = false; + BlockNoteEditor.create({ + extensions: [ + (e) => + new (class extends BlockNoteExtension { + public static key() { + return "test"; + } + constructor(editor: BlockNoteEditor) { + super(editor); + editor.onCreate(() => { + created = true; + }); + } + })(e), + ], + }); + expect(created).toBe(true); +}); diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 0ef8ba10ba..7745cee1d9 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1577,9 +1577,11 @@ export class BlockNoteEditor< * A callback function that runs when the editor has been initialized. * * This can be useful for plugins to initialize themselves after the editor has been initialized. + * + * @param callback The callback to execute. + * @returns A function to remove the callback. */ public onCreate(callback: () => void) { - // TODO I think this create handler is wrong actually... this.on("create", callback); return () => { @@ -1587,6 +1589,42 @@ export class BlockNoteEditor< }; } + /** + * A callback function that runs when the editor has been mounted. + * + * This can be useful for plugins to initialize themselves after the editor has been mounted. + * + * @param callback The callback to execute. + * @returns A function to remove the callback. + */ + public onMount( + callback: (ctx: { + editor: BlockNoteEditor; + }) => void, + ) { + this._eventManager.onMount(callback); + } + + /** + * A callback function that runs when the editor has been unmounted. + * + * This can be useful for plugins to clean up themselves after the editor has been unmounted. + * + * @param callback The callback to execute. + * @returns A function to remove the callback. + */ + public onUnmount( + callback: (ctx: { + editor: BlockNoteEditor; + }) => void, + ) { + this._eventManager.onUnmount(callback); + } + + /** + * Gets the bounding box of the current selection. + * @returns The bounding box of the current selection. + */ public getSelectionBoundingBox() { return this._selectionManager.getSelectionBoundingBox(); } diff --git a/packages/core/src/editor/managers/EventManager.ts b/packages/core/src/editor/managers/EventManager.ts index abd5fc5860..abb2d1f3fb 100644 --- a/packages/core/src/editor/managers/EventManager.ts +++ b/packages/core/src/editor/managers/EventManager.ts @@ -4,6 +4,7 @@ import { type BlocksChanged, } from "../../api/getBlocksChangedByTransaction.js"; import { Transaction } from "prosemirror-state"; +import { EventEmitter } from "../../util/EventEmitter.js"; /** * A function that can be used to unsubscribe from an event. @@ -13,8 +14,50 @@ export type Unsubscribe = () => void; /** * EventManager is a class which manages the events of the editor */ -export class EventManager { - constructor(private editor: Editor) {} +export class EventManager extends EventEmitter<{ + onChange: [ + editor: Editor, + ctx: { + getChanges(): BlocksChanged< + Editor["schema"]["blockSchema"], + Editor["schema"]["inlineContentSchema"], + Editor["schema"]["styleSchema"] + >; + }, + ]; + onSelectionChange: [ctx: { editor: Editor; transaction: Transaction }]; + onMount: [ctx: { editor: Editor }]; + onUnmount: [ctx: { editor: Editor }]; +}> { + constructor(private editor: Editor) { + super(); + // We register tiptap events only once the editor is finished initializing + // otherwise we would be trying to register events on a tiptap editor which does not exist yet + editor.onCreate(() => { + editor._tiptapEditor.on( + "update", + ({ transaction, appendedTransactions }) => { + this.emit("onChange", editor, { + getChanges() { + return getBlocksChangedByTransaction( + transaction, + appendedTransactions, + ); + }, + }); + }, + ); + editor._tiptapEditor.on("selectionUpdate", ({ transaction }) => { + this.emit("onSelectionChange", { editor, transaction }); + }); + editor._tiptapEditor.on("mount", () => { + this.emit("onMount", { editor }); + }); + editor._tiptapEditor.on("unmount", () => { + this.emit("onUnmount", { editor }); + }); + }); + } /** * Register a callback that will be called when the editor changes. @@ -31,27 +74,10 @@ export class EventManager { }, ) => void, ): Unsubscribe { - const cb = ({ - transaction, - appendedTransactions, - }: { - transaction: Transaction; - appendedTransactions: Transaction[]; - }) => { - callback(this.editor, { - getChanges() { - return getBlocksChangedByTransaction( - transaction, - appendedTransactions, - ); - }, - }); - }; - - this.editor._tiptapEditor.on("update", cb); + this.on("onChange", callback); return () => { - this.editor._tiptapEditor.off("update", cb); + this.off("onChange", callback); }; } @@ -77,10 +103,10 @@ export class EventManager { callback(this.editor); }; - this.editor._tiptapEditor.on("selectionUpdate", cb); + this.on("onSelectionChange", cb); return () => { - this.editor._tiptapEditor.off("selectionUpdate", cb); + this.off("onSelectionChange", cb); }; } @@ -88,14 +114,10 @@ export class EventManager { * Register a callback that will be called when the editor is mounted. */ public onMount(callback: (ctx: { editor: Editor }) => void): Unsubscribe { - const cb = () => { - callback({ editor: this.editor }); - }; - - this.editor._tiptapEditor.on("mount", cb); + this.on("onMount", callback); return () => { - this.editor._tiptapEditor.off("mount", cb); + this.off("onMount", callback); }; } @@ -103,14 +125,10 @@ export class EventManager { * Register a callback that will be called when the editor is unmounted. */ public onUnmount(callback: (ctx: { editor: Editor }) => void): Unsubscribe { - const cb = () => { - callback({ editor: this.editor }); - }; - - this.editor._tiptapEditor.on("unmount", cb); + this.on("onUnmount", callback); return () => { - this.editor._tiptapEditor.off("unmount", cb); + this.off("onUnmount", callback); }; } } From 2318000d88162bdc09252891e3c00848a8959ebe Mon Sep 17 00:00:00 2001 From: Nick Perez Date: Wed, 8 Oct 2025 09:38:07 +0200 Subject: [PATCH 2/2] chore: update desc --- docs/content/docs/reference/editor/events.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/docs/reference/editor/events.mdx b/docs/content/docs/reference/editor/events.mdx index f8cd85822a..61349c79c9 100644 --- a/docs/content/docs/reference/editor/events.mdx +++ b/docs/content/docs/reference/editor/events.mdx @@ -12,7 +12,7 @@ BlockNote provides several event callbacks that allow you to respond to changes The editor emits events for: -- **Editor lifecycle** - When the editor is mounted, unmounted, etc. +- **Editor lifecycle** - When the editor is created, mounted, unmounted, etc. - **Content changes** - When blocks are inserted, updated, or deleted - **Selection changes** - When the cursor position or selection changes