diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 8008312c3..6cfef88fc 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -120,6 +120,7 @@ import { EventEmitter } from "../util/EventEmitter.js"; import { BlockNoteExtension } from "./BlockNoteExtension.js"; import "../style.css"; +import { BlockChangePlugin } from "../extensions/BlockChange/BlockChangePlugin.js"; /** * A factory function that returns a BlockNoteExtension @@ -1588,6 +1589,32 @@ export class BlockNoteEditor< (this.extensions["yCursorPlugin"] as CursorPlugin).updateUser(user); } + /** + * Registers a callback which will be called before any change is applied to the editor, allowing you to cancel the change. + */ + public beforeChange( + /** + * If the callback returns `false`, the change will be canceled & not applied to the editor. + */ + callback: ( + editor: BlockNoteEditor, + context: { + getChanges: () => BlocksChanged; + tr: Transaction; + }, + ) => boolean | void, + ): () => void { + if (this.headless) { + return () => { + // noop + }; + } + + return (this.extensions["blockChange"] as BlockChangePlugin).subscribe( + (context) => callback(this, context), + ); + } + /** * A callback function that runs whenever the editor's contents change. * diff --git a/packages/core/src/editor/BlockNoteExtensions.ts b/packages/core/src/editor/BlockNoteExtensions.ts index 6bd04b333..f6768d594 100644 --- a/packages/core/src/editor/BlockNoteExtensions.ts +++ b/packages/core/src/editor/BlockNoteExtensions.ts @@ -11,6 +11,7 @@ import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboar import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js"; import type { ThreadStore } from "../comments/index.js"; import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js"; +import { BlockChangePlugin } from "../extensions/BlockChange/BlockChangePlugin.js"; import { CursorPlugin } from "../extensions/Collaboration/CursorPlugin.js"; import { SyncPlugin } from "../extensions/Collaboration/SyncPlugin.js"; import { UndoPlugin } from "../extensions/Collaboration/UndoPlugin.js"; @@ -150,6 +151,7 @@ export const getBlockNoteExtensions = < } ret["nodeSelectionKeyboard"] = new NodeSelectionKeyboardPlugin(); + ret["blockChange"] = new BlockChangePlugin(); ret["showSelection"] = new ShowSelectionPlugin(opts.editor); diff --git a/packages/core/src/extensions/BlockChange/BlockChangePlugin.ts b/packages/core/src/extensions/BlockChange/BlockChangePlugin.ts new file mode 100644 index 000000000..51fe89b0f --- /dev/null +++ b/packages/core/src/extensions/BlockChange/BlockChangePlugin.ts @@ -0,0 +1,66 @@ +import { Plugin, Transaction } from "prosemirror-state"; +import { getBlocksChangedByTransaction } from "../../api/nodeUtil.js"; +import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js"; +import { BlocksChanged } from "../../index.js"; + +/** + * This plugin can filter transactions before they are applied to the editor, but with a higher-level API than `filterTransaction` from prosemirror. + */ +export class BlockChangePlugin extends BlockNoteExtension { + public static key() { + return "blockChange"; + } + + private beforeChangeCallbacks: ((context: { + getChanges: () => BlocksChanged; + tr: Transaction; + }) => boolean | void)[] = []; + + constructor() { + super(); + + this.addProsemirrorPlugin( + new Plugin({ + filterTransaction: (tr) => { + let changes: + | ReturnType + | undefined = undefined; + + return this.beforeChangeCallbacks.reduce((acc, cb) => { + if (acc === false) { + // We only care that we hit a `false` result, so we can stop iterating. + return acc; + } + return ( + cb({ + getChanges() { + if (changes) { + return changes; + } + changes = getBlocksChangedByTransaction(tr); + return changes; + }, + tr, + }) !== false + ); + }, true); + }, + }), + ); + } + + public subscribe( + callback: (context: { + getChanges: () => BlocksChanged; + tr: Transaction; + }) => boolean | void, + ) { + this.beforeChangeCallbacks.push(callback); + + return () => { + this.beforeChangeCallbacks = this.beforeChangeCallbacks.filter( + (cb) => cb !== callback, + ); + }; + } +}