Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,30 @@ describe("ExtensionManager ordering", () => {
);
});
});

describe("ExtensionManager runtime registration", () => {
it("installs the plugins of blockNoteExtensions declared by an extension registered at runtime", () => {
const subKey = new PluginKey("runtime-sub");
const parentKey = new PluginKey("runtime-parent");

const sub = createExtension(() => ({
key: "runtime-sub",
prosemirrorPlugins: [new Plugin({ key: subKey })],
}));
const parent = createExtension(() => ({
key: "runtime-parent",
blockNoteExtensions: [sub()],
prosemirrorPlugins: [new Plugin({ key: parentKey })],
}));

const editor = createMountedEditor([]);
editor.registerExtension(parent());

expect(editor.getExtension(sub)).toBeDefined();
expect(pluginIndex(editor, subKey)).not.toBe(-1);
// Same order as when the extension is passed to the editor up front.
expect(pluginIndex(editor, subKey)).toBeLessThan(
pluginIndex(editor, parentKey),
);
});
});
38 changes: 26 additions & 12 deletions packages/core/src/editor/managers/ExtensionManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,15 @@ export class ExtensionManager {
.concat(toRegister)
.filter(Boolean) as (Extension | ExtensionFactoryInstance)[];

const registeredExtensions = newExtensions
.map((ext) => this.addExtension(ext))
.filter(Boolean) as Extension[];
const extensionsBefore = new Set(this.extensions);
newExtensions.forEach((ext) => this.addExtension(ext));
// `addExtension` also registers the sub-extensions declared in
// `blockNoteExtensions`, so take everything it added, in the same order
// their plugins get when the editor is created.
const getPriority = this.getExtensionPriority();
const registeredExtensions = this.extensions
.filter((extension) => !extensionsBefore.has(extension))
.sort((a, b) => getPriority(b.key) - getPriority(a.key));

const pluginsToAdd: Plugin[] = [];
for (const extension of registeredExtensions) {
Expand Down Expand Up @@ -402,16 +408,11 @@ export class ExtensionManager {
}

/**
* Get all the extensions that are registered to the editor
* Returns a function that gives the priority of a registered extension
* (higher runs first), based on `runsBefore` and `blockNoteExtensions`.
*/
public getTiptapExtensions(): AnyTiptapExtension[] {
// Start with the default tiptap extensions
const tiptapExtensions = getDefaultTiptapExtensions(
this.editor,
this.options,
).filter((extension) => !this.disabledExtensions.has(extension.name));

const getPriority = sortByDependencies(
private getExtensionPriority() {
return sortByDependencies(
this.extensions.map((extension) => {
// A sub-extension declared via `blockNoteExtensions` must run before the
// extension(s) that declared it, so we merge those parents into its
Expand All @@ -426,6 +427,19 @@ export class ExtensionManager {
};
}),
);
}

/**
* Get all the extensions that are registered to the editor
*/
public getTiptapExtensions(): AnyTiptapExtension[] {
// Start with the default tiptap extensions
const tiptapExtensions = getDefaultTiptapExtensions(
this.editor,
this.options,
).filter((extension) => !this.disabledExtensions.has(extension.name));

const getPriority = this.getExtensionPriority();

const inputRulesByPriority = new Map<number, InputRule[]>();
for (const extension of this.extensions) {
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/yjs/extensions/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @vitest-environment jsdom
*/
import { describe, expect, it } from "vite-plus/test";
import { Awareness } from "y-protocols/awareness";
import * as Y from "yjs";

import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import { CollaborationExtension } from "./index.js";

describe("CollaborationExtension", () => {
it("syncs edits when registered on an existing editor", () => {
const doc = new Y.Doc();
const fragment = doc.getXmlFragment("doc");
const editor = BlockNoteEditor.create({
disableExtensions: ["history"],
initialContent: [{ type: "paragraph", id: "initialBlockId" }],
});
editor.mount(document.createElement("div"));

editor.registerExtension(
CollaborationExtension({
fragment,
user: { name: "Test User", color: "#FF0000" },
provider: { awareness: new Awareness(doc) },
}),
);
editor.replaceBlocks(editor.document, [
{ type: "paragraph", content: "hello" },
]);

expect(fragment.toJSON()).toContain("hello");

editor.unmount();
doc.destroy();
});
});
Loading