Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/content/docs/reference/editor/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/editor/BlockNoteEditor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getNearestBlockPos,
} from "../api/getBlockInfoFromPos.js";
import { BlockNoteEditor } from "./BlockNoteEditor.js";
import { BlockNoteExtension } from "./BlockNoteExtension.js";

/**
* @vitest-environment jsdom
Expand Down Expand Up @@ -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);
});
40 changes: 39 additions & 1 deletion packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,16 +1577,54 @@ 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 () => {
this.off("create", callback);
};
}

/**
* 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<BSchema, ISchema, SSchema>;
}) => 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<BSchema, ISchema, SSchema>;
}) => 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();
}
Expand Down
88 changes: 53 additions & 35 deletions packages/core/src/editor/managers/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -13,8 +14,50 @@ export type Unsubscribe = () => void;
/**
* EventManager is a class which manages the events of the editor
*/
export class EventManager<Editor extends BlockNoteEditor> {
constructor(private editor: Editor) {}
export class EventManager<Editor extends BlockNoteEditor> 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.
Expand All @@ -31,27 +74,10 @@ export class EventManager<Editor extends BlockNoteEditor> {
},
) => 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);
};
}

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

this.editor._tiptapEditor.on("selectionUpdate", cb);
this.on("onSelectionChange", cb);

return () => {
this.editor._tiptapEditor.off("selectionUpdate", cb);
this.off("onSelectionChange", cb);
};
}

/**
* 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);
};
}

/**
* 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);
};
}
}
Loading