Skip to content

Commit aec52e8

Browse files
committed
Merge branch 'ui-plugins' into menus
2 parents 4e1aaa5 + 1acc302 commit aec52e8

File tree

19 files changed

+691
-86
lines changed

19 files changed

+691
-86
lines changed

examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const FileReplaceButton = () => {
7070
variant={"panel-popover"}
7171
>
7272
{/* Replaces default file panel with our Uppy one. */}
73-
<UppyFilePanel block={block as any} />
73+
<UppyFilePanel blockId={block.id} />
7474
</Components.Generic.Popover.Content>
7575
</Components.Generic.Popover.Root>
7676
);

examples/03-ui-components/11-uppy-file-panel/src/UppyFilePanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const uppy = new Uppy()
4343
});
4444

4545
export function UppyFilePanel(props: FilePanelProps) {
46-
const { block } = props;
46+
const { blockId } = props;
4747
const editor = useBlockNoteEditor();
4848

4949
useEffect(() => {
@@ -68,7 +68,7 @@ export function UppyFilePanel(props: FilePanelProps) {
6868
url: response.uploadURL,
6969
},
7070
};
71-
editor.updateBlock(block, updateData);
71+
editor.updateBlock(blockId, updateData);
7272

7373
// File should be removed from the Uppy instance after upload.
7474
uppy.removeFile(file.id);
@@ -78,7 +78,7 @@ export function UppyFilePanel(props: FilePanelProps) {
7878
return () => {
7979
uppy.off("upload-success", handler);
8080
};
81-
}, [block, editor]);
81+
}, [blockId, editor]);
8282

8383
// set up dashboard as in https://uppy.io/examples/
8484
return <Dashboard uppy={uppy} width={400} height={500} />;

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"dependencies": {
8282
"@emoji-mart/data": "^1.2.1",
8383
"@shikijs/types": "3.13.0",
84+
"@tanstack/store": "0.7.7",
8485
"@tiptap/core": "^3.7.2",
8586
"@tiptap/extension-bold": "^3.7.2",
8687
"@tiptap/extension-code": "^3.7.2",

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import { updateBlockTr } from "../api/blockManipulation/commands/updateBlock/upd
7474
import { getBlockInfoFromTransaction } from "../api/getBlockInfoFromPos.js";
7575
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
7676
import "../style.css";
77+
import { ExtensionFactory } from "./managers/extensions/types.js";
7778

7879
/**
7980
* A factory function that returns a BlockNoteExtension
@@ -1026,9 +1027,35 @@ export class BlockNoteEditor<
10261027
ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
10271028
key = ext.key(),
10281029
): T {
1029-
return this._extensionManager.extension(ext, key);
1030+
return this._extensionManager.getExtension(key) as any;
10301031
}
10311032

1033+
/**
1034+
* Add an extension to the editor
1035+
* @param extension The extension to add
1036+
* @returns The extension instance
1037+
*/
1038+
public addExtension(
1039+
extension: ReturnType<ExtensionFactory> | ExtensionFactory,
1040+
) {
1041+
return this._extensionManager.addExtension(extension);
1042+
}
1043+
1044+
public getExtension<
1045+
T extends ExtensionFactory | ReturnType<ExtensionFactory> | string,
1046+
>(
1047+
extension: T,
1048+
):
1049+
| (T extends ExtensionFactory
1050+
? ReturnType<T>
1051+
: T extends ReturnType<ExtensionFactory>
1052+
? T
1053+
: T extends string
1054+
? ReturnType<ExtensionFactory>
1055+
: never)
1056+
| undefined {
1057+
return this._extensionManager.getExtension(extension);
1058+
}
10321059
/**
10331060
* Mount the editor to a DOM element.
10341061
*
@@ -1047,7 +1074,18 @@ export class BlockNoteEditor<
10471074
return;
10481075
}
10491076

1050-
this._tiptapEditor.mount({ mount: element });
1077+
const extensions = this._extensionManager.getExtensions().values();
1078+
// TODO can do something similar for input rules
1079+
// extensions.filter(e => e.instance.inputRules)
1080+
1081+
const state = this._tiptapEditor.state.reconfigure({
1082+
plugins: this._tiptapEditor.state.plugins.concat(
1083+
extensions.flatMap((e) => e.instance.plugins ?? []).toArray(),
1084+
),
1085+
});
1086+
this._tiptapEditor.view.updateState(state);
1087+
1088+
this._tiptapEditor.mount({ mount: element } as any);
10511089
};
10521090

10531091
/**
@@ -1598,6 +1636,17 @@ export class BlockNoteEditor<
15981636
);
15991637
}
16001638

1639+
public getBlockClientRect(blockId: string): DOMRect | undefined {
1640+
const blockElement = this.prosemirrorView.root.querySelector(
1641+
`[data-node-type="blockContainer"][data-id="${blockId}"]`,
1642+
);
1643+
if (!blockElement) {
1644+
return;
1645+
}
1646+
1647+
return blockElement.getBoundingClientRect();
1648+
}
1649+
16011650
/**
16021651
* A callback function that runs when the editor has been initialized.
16031652
*

packages/core/src/editor/managers/ExtensionManager.ts

Lines changed: 172 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,173 @@ import { SuggestionMenuProseMirrorPlugin } from "../../extensions/SuggestionMenu
77
import { TableHandlesProsemirrorPlugin } from "../../extensions/TableHandles/TableHandlesPlugin.js";
88
import { BlockNoteExtension } from "../BlockNoteExtension.js";
99
import { BlockNoteEditor } from "../BlockNoteEditor.js";
10+
import { Extension, ExtensionFactory } from "./extensions/types.js";
1011

1112
export class ExtensionManager {
12-
constructor(private editor: BlockNoteEditor) {}
13+
private extensions: Map<
14+
string,
15+
{
16+
instance: Extension;
17+
unmount: () => void;
18+
abortController: AbortController;
19+
}
20+
> = new Map();
21+
private extensionFactories: WeakMap<ExtensionFactory, Extension> =
22+
new WeakMap();
23+
constructor(private editor: BlockNoteEditor) {
24+
editor.onMount(() => {
25+
for (const extension of this.extensions.values()) {
26+
if (extension.instance.init) {
27+
const unmountCallback = extension.instance.init({
28+
dom: editor.prosemirrorView.dom,
29+
root: editor.prosemirrorView.root,
30+
abortController: extension.abortController,
31+
});
32+
extension.unmount = () => {
33+
unmountCallback?.();
34+
extension.abortController.abort();
35+
};
36+
}
37+
}
38+
});
39+
40+
editor.onUnmount(() => {
41+
for (const extension of this.extensions.values()) {
42+
if (extension.unmount) {
43+
extension.unmount();
44+
}
45+
}
46+
});
47+
}
1348

1449
/**
15-
* Shorthand to get a typed extension from the editor, by
16-
* just passing in the extension class.
17-
*
18-
* @param ext - The extension class to get
19-
* @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
20-
* @returns The extension instance
50+
* Get all extensions
2151
*/
22-
public extension<T extends BlockNoteExtension>(
23-
ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
24-
key = ext.key(),
25-
): T {
26-
const extension = this.editor.extensions[key] as T;
27-
if (!extension) {
28-
throw new Error(`Extension ${key} not found`);
52+
public getExtensions() {
53+
return this.extensions;
54+
}
55+
56+
/**
57+
* Add an extension to the editor after initialization
58+
*/
59+
public addExtension<T extends ExtensionFactory | Extension>(
60+
extension: T,
61+
): T extends ExtensionFactory ? ReturnType<T> : T {
62+
if (
63+
typeof extension === "function" &&
64+
this.extensionFactories.has(extension)
65+
) {
66+
return this.extensionFactories.get(extension) as any;
2967
}
30-
return extension;
68+
69+
if (
70+
typeof extension === "object" &&
71+
"key" in extension &&
72+
this.extensions.has(extension.key)
73+
) {
74+
return this.extensions.get(extension.key) as any;
75+
}
76+
77+
const abortController = new AbortController();
78+
let instance: Extension;
79+
if (typeof extension === "function") {
80+
instance = extension(this.editor);
81+
this.extensionFactories.set(extension, instance);
82+
} else {
83+
instance = extension;
84+
}
85+
86+
let unmountCallback: undefined | (() => void) = undefined;
87+
88+
this.extensions.set(instance.key, {
89+
instance,
90+
unmount: () => {
91+
unmountCallback?.();
92+
abortController.abort();
93+
},
94+
abortController,
95+
});
96+
97+
for (const plugin of instance.plugins || []) {
98+
this.editor._tiptapEditor.registerPlugin(plugin);
99+
}
100+
101+
if ("inputRules" in instance) {
102+
// TODO do we need to add new input rules to the editor?
103+
// And other things?
104+
}
105+
106+
if (!this.editor.headless && instance.init) {
107+
unmountCallback =
108+
instance.init({
109+
dom: this.editor.prosemirrorView.dom,
110+
root: this.editor.prosemirrorView.root,
111+
abortController,
112+
}) || undefined;
113+
}
114+
115+
return instance as any;
31116
}
32117

33118
/**
34-
* Get all extensions
119+
* Remove an extension from the editor
120+
* @param extension - The extension to remove
121+
* @returns The extension that was removed
35122
*/
36-
public getExtensions() {
37-
return this.editor.extensions;
123+
public removeExtension<T extends ExtensionFactory | Extension | string>(
124+
extension: T,
125+
): undefined {
126+
let extensionKey: string | undefined;
127+
if (typeof extension === "string") {
128+
extensionKey = extension;
129+
} else if (typeof extension === "function") {
130+
extensionKey = this.extensionFactories.get(extension)?.key;
131+
} else {
132+
extensionKey = extension.key;
133+
}
134+
if (!extensionKey) {
135+
return undefined;
136+
}
137+
const extensionToDelete = this.extensions.get(extensionKey);
138+
if (extensionToDelete) {
139+
if (extensionToDelete.unmount) {
140+
extensionToDelete.unmount();
141+
}
142+
this.extensions.delete(extensionKey);
143+
}
38144
}
39145

40146
/**
41-
* Get a specific extension by key
147+
* Get a specific extension by it's instance
42148
*/
43-
public getExtension(key: string) {
44-
return this.editor.extensions[key];
149+
public getExtension<T extends ExtensionFactory | Extension | string>(
150+
extension: T,
151+
):
152+
| (T extends ExtensionFactory
153+
? ReturnType<T>
154+
: T extends Extension
155+
? T
156+
: T extends string
157+
? Extension
158+
: never)
159+
| undefined {
160+
if (typeof extension === "string") {
161+
if (!this.extensions.has(extension)) {
162+
return undefined;
163+
}
164+
return this.extensions.get(extension) as any;
165+
} else if (typeof extension === "function") {
166+
if (!this.extensionFactories.has(extension)) {
167+
return undefined;
168+
}
169+
return this.extensionFactories.get(extension) as any;
170+
} else if (typeof extension === "object" && "key" in extension) {
171+
if (!this.extensions.has(extension.key)) {
172+
return undefined;
173+
}
174+
return this.extensions.get(extension.key) as any;
175+
}
176+
throw new Error(`Invalid extension type: ${typeof extension}`);
45177
}
46178

47179
/**
@@ -51,6 +183,25 @@ export class ExtensionManager {
51183
return key in this.editor.extensions;
52184
}
53185

186+
/**
187+
* Shorthand to get a typed extension from the editor, by
188+
* just passing in the extension class.
189+
*
190+
* @param ext - The extension class to get
191+
* @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
192+
* @returns The extension instance
193+
*/
194+
public extension<T extends BlockNoteExtension>(
195+
ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
196+
key = ext.key(),
197+
): T {
198+
const extension = this.editor.extensions[key] as T;
199+
if (!extension) {
200+
throw new Error(`Extension ${key} not found`);
201+
}
202+
return extension;
203+
}
204+
54205
// Plugin getters - these provide access to the core BlockNote plugins
55206

56207
/**

0 commit comments

Comments
 (0)