diff --git a/packages/core/src/editor/managers/ExtensionManager/ExtensionManager.test.ts b/packages/core/src/editor/managers/ExtensionManager/ExtensionManager.test.ts index 5e0edadae5..2ec705ab42 100644 --- a/packages/core/src/editor/managers/ExtensionManager/ExtensionManager.test.ts +++ b/packages/core/src/editor/managers/ExtensionManager/ExtensionManager.test.ts @@ -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), + ); + }); +}); diff --git a/packages/core/src/editor/managers/ExtensionManager/index.ts b/packages/core/src/editor/managers/ExtensionManager/index.ts index 5a3929e8cf..2af8020eeb 100644 --- a/packages/core/src/editor/managers/ExtensionManager/index.ts +++ b/packages/core/src/editor/managers/ExtensionManager/index.ts @@ -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) { @@ -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 @@ -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(); for (const extension of this.extensions) { diff --git a/packages/core/src/yjs/extensions/index.test.ts b/packages/core/src/yjs/extensions/index.test.ts new file mode 100644 index 0000000000..9e59f220df --- /dev/null +++ b/packages/core/src/yjs/extensions/index.test.ts @@ -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(); + }); +});