Skip to content

feat(block-change): adds a new API for blocking changes to editor state, by filtering transactions #1750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
27 changes: 27 additions & 0 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<BSchema, ISchema, SSchema>,
context: {
getChanges: () => BlocksChanged<BSchema, ISchema, SSchema>;
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.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/editor/BlockNoteExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -150,6 +151,7 @@ export const getBlockNoteExtensions = <
}

ret["nodeSelectionKeyboard"] = new NodeSelectionKeyboardPlugin();
ret["blockChange"] = new BlockChangePlugin();

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

Expand Down
66 changes: 66 additions & 0 deletions packages/core/src/extensions/BlockChange/BlockChangePlugin.ts
Original file line number Diff line number Diff line change
@@ -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<any, any, any>;
tr: Transaction;
}) => boolean | void)[] = [];

constructor() {
super();

this.addProsemirrorPlugin(
new Plugin({
filterTransaction: (tr) => {
let changes:
| ReturnType<typeof getBlocksChangedByTransaction>
| 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<any, any, any>;
tr: Transaction;
}) => boolean | void,
) {
this.beforeChangeCallbacks.push(callback);

return () => {
this.beforeChangeCallbacks = this.beforeChangeCallbacks.filter(
(cb) => cb !== callback,
);
};
}
}
Loading