Skip to content

Commit ce88b2d

Browse files
authored
fix: allow listening to onChange and other events before the underlying editor is initialized (#2063)
1 parent 687bf1e commit ce88b2d

File tree

4 files changed

+153
-37
lines changed

4 files changed

+153
-37
lines changed

docs/content/docs/reference/editor/events.mdx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ BlockNote provides several event callbacks that allow you to respond to changes
1212

1313
The editor emits events for:
1414

15-
- **Editor initialization** - When the editor is ready for use
15+
- **Editor lifecycle** - When the editor is created, mounted, unmounted, etc.
1616
- **Content changes** - When blocks are inserted, updated, or deleted
1717
- **Selection changes** - When the cursor position or selection changes
1818

@@ -27,6 +27,26 @@ editor.onCreate(() => {
2727
});
2828
```
2929

30+
## `onMount`
31+
32+
The `onMount` callback is called when the editor has been mounted.
33+
34+
```typescript
35+
editor.onMount(() => {
36+
console.log("Editor is mounted");
37+
});
38+
```
39+
40+
## `onUnmount`
41+
42+
The `onUnmount` callback is called when the editor has been unmounted.
43+
44+
```typescript
45+
editor.onUnmount(() => {
46+
console.log("Editor is unmounted");
47+
});
48+
```
49+
3050
## `onSelectionChange`
3151

3252
The `onSelectionChange` callback is called whenever the editor's selection changes, including cursor movements and text selections.

packages/core/src/editor/BlockNoteEditor.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getNearestBlockPos,
55
} from "../api/getBlockInfoFromPos.js";
66
import { BlockNoteEditor } from "./BlockNoteEditor.js";
7+
import { BlockNoteExtension } from "./BlockNoteExtension.js";
78

89
/**
910
* @vitest-environment jsdom
@@ -102,3 +103,42 @@ it("block prop types", () => {
102103
expect(level).toBe(1);
103104
}
104105
});
106+
107+
it("onMount and onUnmount", () => {
108+
const editor = BlockNoteEditor.create();
109+
let mounted = false;
110+
let unmounted = false;
111+
editor.onMount(() => {
112+
mounted = true;
113+
});
114+
editor.onUnmount(() => {
115+
unmounted = true;
116+
});
117+
editor.mount(document.createElement("div"));
118+
expect(mounted).toBe(true);
119+
expect(unmounted).toBe(false);
120+
editor.unmount();
121+
expect(mounted).toBe(true);
122+
expect(unmounted).toBe(true);
123+
});
124+
125+
it("onCreate event", () => {
126+
let created = false;
127+
BlockNoteEditor.create({
128+
extensions: [
129+
(e) =>
130+
new (class extends BlockNoteExtension {
131+
public static key() {
132+
return "test";
133+
}
134+
constructor(editor: BlockNoteEditor) {
135+
super(editor);
136+
editor.onCreate(() => {
137+
created = true;
138+
});
139+
}
140+
})(e),
141+
],
142+
});
143+
expect(created).toBe(true);
144+
});

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1577,16 +1577,54 @@ export class BlockNoteEditor<
15771577
* A callback function that runs when the editor has been initialized.
15781578
*
15791579
* This can be useful for plugins to initialize themselves after the editor has been initialized.
1580+
*
1581+
* @param callback The callback to execute.
1582+
* @returns A function to remove the callback.
15801583
*/
15811584
public onCreate(callback: () => void) {
1582-
// TODO I think this create handler is wrong actually...
15831585
this.on("create", callback);
15841586

15851587
return () => {
15861588
this.off("create", callback);
15871589
};
15881590
}
15891591

1592+
/**
1593+
* A callback function that runs when the editor has been mounted.
1594+
*
1595+
* This can be useful for plugins to initialize themselves after the editor has been mounted.
1596+
*
1597+
* @param callback The callback to execute.
1598+
* @returns A function to remove the callback.
1599+
*/
1600+
public onMount(
1601+
callback: (ctx: {
1602+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
1603+
}) => void,
1604+
) {
1605+
this._eventManager.onMount(callback);
1606+
}
1607+
1608+
/**
1609+
* A callback function that runs when the editor has been unmounted.
1610+
*
1611+
* This can be useful for plugins to clean up themselves after the editor has been unmounted.
1612+
*
1613+
* @param callback The callback to execute.
1614+
* @returns A function to remove the callback.
1615+
*/
1616+
public onUnmount(
1617+
callback: (ctx: {
1618+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>;
1619+
}) => void,
1620+
) {
1621+
this._eventManager.onUnmount(callback);
1622+
}
1623+
1624+
/**
1625+
* Gets the bounding box of the current selection.
1626+
* @returns The bounding box of the current selection.
1627+
*/
15901628
public getSelectionBoundingBox() {
15911629
return this._selectionManager.getSelectionBoundingBox();
15921630
}

packages/core/src/editor/managers/EventManager.ts

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type BlocksChanged,
55
} from "../../api/getBlocksChangedByTransaction.js";
66
import { Transaction } from "prosemirror-state";
7+
import { EventEmitter } from "../../util/EventEmitter.js";
78

89
/**
910
* A function that can be used to unsubscribe from an event.
@@ -13,8 +14,50 @@ export type Unsubscribe = () => void;
1314
/**
1415
* EventManager is a class which manages the events of the editor
1516
*/
16-
export class EventManager<Editor extends BlockNoteEditor> {
17-
constructor(private editor: Editor) {}
17+
export class EventManager<Editor extends BlockNoteEditor> extends EventEmitter<{
18+
onChange: [
19+
editor: Editor,
20+
ctx: {
21+
getChanges(): BlocksChanged<
22+
Editor["schema"]["blockSchema"],
23+
Editor["schema"]["inlineContentSchema"],
24+
Editor["schema"]["styleSchema"]
25+
>;
26+
},
27+
];
28+
onSelectionChange: [ctx: { editor: Editor; transaction: Transaction }];
29+
onMount: [ctx: { editor: Editor }];
30+
onUnmount: [ctx: { editor: Editor }];
31+
}> {
32+
constructor(private editor: Editor) {
33+
super();
34+
// We register tiptap events only once the editor is finished initializing
35+
// otherwise we would be trying to register events on a tiptap editor which does not exist yet
36+
editor.onCreate(() => {
37+
editor._tiptapEditor.on(
38+
"update",
39+
({ transaction, appendedTransactions }) => {
40+
this.emit("onChange", editor, {
41+
getChanges() {
42+
return getBlocksChangedByTransaction(
43+
transaction,
44+
appendedTransactions,
45+
);
46+
},
47+
});
48+
},
49+
);
50+
editor._tiptapEditor.on("selectionUpdate", ({ transaction }) => {
51+
this.emit("onSelectionChange", { editor, transaction });
52+
});
53+
editor._tiptapEditor.on("mount", () => {
54+
this.emit("onMount", { editor });
55+
});
56+
editor._tiptapEditor.on("unmount", () => {
57+
this.emit("onUnmount", { editor });
58+
});
59+
});
60+
}
1861

1962
/**
2063
* Register a callback that will be called when the editor changes.
@@ -31,27 +74,10 @@ export class EventManager<Editor extends BlockNoteEditor> {
3174
},
3275
) => void,
3376
): Unsubscribe {
34-
const cb = ({
35-
transaction,
36-
appendedTransactions,
37-
}: {
38-
transaction: Transaction;
39-
appendedTransactions: Transaction[];
40-
}) => {
41-
callback(this.editor, {
42-
getChanges() {
43-
return getBlocksChangedByTransaction(
44-
transaction,
45-
appendedTransactions,
46-
);
47-
},
48-
});
49-
};
50-
51-
this.editor._tiptapEditor.on("update", cb);
77+
this.on("onChange", callback);
5278

5379
return () => {
54-
this.editor._tiptapEditor.off("update", cb);
80+
this.off("onChange", callback);
5581
};
5682
}
5783

@@ -77,40 +103,32 @@ export class EventManager<Editor extends BlockNoteEditor> {
77103
callback(this.editor);
78104
};
79105

80-
this.editor._tiptapEditor.on("selectionUpdate", cb);
106+
this.on("onSelectionChange", cb);
81107

82108
return () => {
83-
this.editor._tiptapEditor.off("selectionUpdate", cb);
109+
this.off("onSelectionChange", cb);
84110
};
85111
}
86112

87113
/**
88114
* Register a callback that will be called when the editor is mounted.
89115
*/
90116
public onMount(callback: (ctx: { editor: Editor }) => void): Unsubscribe {
91-
const cb = () => {
92-
callback({ editor: this.editor });
93-
};
94-
95-
this.editor._tiptapEditor.on("mount", cb);
117+
this.on("onMount", callback);
96118

97119
return () => {
98-
this.editor._tiptapEditor.off("mount", cb);
120+
this.off("onMount", callback);
99121
};
100122
}
101123

102124
/**
103125
* Register a callback that will be called when the editor is unmounted.
104126
*/
105127
public onUnmount(callback: (ctx: { editor: Editor }) => void): Unsubscribe {
106-
const cb = () => {
107-
callback({ editor: this.editor });
108-
};
109-
110-
this.editor._tiptapEditor.on("unmount", cb);
128+
this.on("onUnmount", callback);
111129

112130
return () => {
113-
this.editor._tiptapEditor.off("unmount", cb);
131+
this.off("onUnmount", callback);
114132
};
115133
}
116134
}

0 commit comments

Comments
 (0)