Skip to content

Commit 8ffc689

Browse files
authored
feat(block-change): adds a new API for blocking changes to editor state, by filtering transactions (#1750)
1 parent 2c9162f commit 8ffc689

File tree

3 files changed

+95
-0
lines changed

3 files changed

+95
-0
lines changed

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ import { EventEmitter } from "../util/EventEmitter.js";
120120
import { BlockNoteExtension } from "./BlockNoteExtension.js";
121121

122122
import "../style.css";
123+
import { BlockChangePlugin } from "../extensions/BlockChange/BlockChangePlugin.js";
123124

124125
/**
125126
* A factory function that returns a BlockNoteExtension
@@ -1596,6 +1597,32 @@ export class BlockNoteEditor<
15961597
(this.extensions["yCursorPlugin"] as CursorPlugin).updateUser(user);
15971598
}
15981599

1600+
/**
1601+
* Registers a callback which will be called before any change is applied to the editor, allowing you to cancel the change.
1602+
*/
1603+
public beforeChange(
1604+
/**
1605+
* If the callback returns `false`, the change will be canceled & not applied to the editor.
1606+
*/
1607+
callback: (
1608+
editor: BlockNoteEditor<BSchema, ISchema, SSchema>,
1609+
context: {
1610+
getChanges: () => BlocksChanged<BSchema, ISchema, SSchema>;
1611+
tr: Transaction;
1612+
},
1613+
) => boolean | void,
1614+
): () => void {
1615+
if (this.headless) {
1616+
return () => {
1617+
// noop
1618+
};
1619+
}
1620+
1621+
return (this.extensions["blockChange"] as BlockChangePlugin).subscribe(
1622+
(context) => callback(this, context),
1623+
);
1624+
}
1625+
15991626
/**
16001627
* A callback function that runs whenever the editor's contents change.
16011628
*

packages/core/src/editor/BlockNoteExtensions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboar
1111
import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
1212
import type { ThreadStore } from "../comments/index.js";
1313
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
14+
import { BlockChangePlugin } from "../extensions/BlockChange/BlockChangePlugin.js";
1415
import { CursorPlugin } from "../extensions/Collaboration/CursorPlugin.js";
1516
import { SyncPlugin } from "../extensions/Collaboration/SyncPlugin.js";
1617
import { UndoPlugin } from "../extensions/Collaboration/UndoPlugin.js";
@@ -146,6 +147,7 @@ export const getBlockNoteExtensions = <
146147
}
147148

148149
ret["nodeSelectionKeyboard"] = new NodeSelectionKeyboardPlugin();
150+
ret["blockChange"] = new BlockChangePlugin();
149151

150152
ret["showSelection"] = new ShowSelectionPlugin(opts.editor);
151153

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Plugin, Transaction } from "prosemirror-state";
2+
import { getBlocksChangedByTransaction } from "../../api/nodeUtil.js";
3+
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
4+
import { BlocksChanged } from "../../index.js";
5+
6+
/**
7+
* This plugin can filter transactions before they are applied to the editor, but with a higher-level API than `filterTransaction` from prosemirror.
8+
*/
9+
export class BlockChangePlugin extends BlockNoteExtension {
10+
public static key() {
11+
return "blockChange";
12+
}
13+
14+
private beforeChangeCallbacks: ((context: {
15+
getChanges: () => BlocksChanged<any, any, any>;
16+
tr: Transaction;
17+
}) => boolean | void)[] = [];
18+
19+
constructor() {
20+
super();
21+
22+
this.addProsemirrorPlugin(
23+
new Plugin({
24+
filterTransaction: (tr) => {
25+
let changes:
26+
| ReturnType<typeof getBlocksChangedByTransaction>
27+
| undefined = undefined;
28+
29+
return this.beforeChangeCallbacks.reduce((acc, cb) => {
30+
if (acc === false) {
31+
// We only care that we hit a `false` result, so we can stop iterating.
32+
return acc;
33+
}
34+
return (
35+
cb({
36+
getChanges() {
37+
if (changes) {
38+
return changes;
39+
}
40+
changes = getBlocksChangedByTransaction(tr);
41+
return changes;
42+
},
43+
tr,
44+
}) !== false
45+
);
46+
}, true);
47+
},
48+
}),
49+
);
50+
}
51+
52+
public subscribe(
53+
callback: (context: {
54+
getChanges: () => BlocksChanged<any, any, any>;
55+
tr: Transaction;
56+
}) => boolean | void,
57+
) {
58+
this.beforeChangeCallbacks.push(callback);
59+
60+
return () => {
61+
this.beforeChangeCallbacks = this.beforeChangeCallbacks.filter(
62+
(cb) => cb !== callback,
63+
);
64+
};
65+
}
66+
}

0 commit comments

Comments
 (0)