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 @@ -70,7 +70,7 @@ export const FileReplaceButton = () => {
variant={"panel-popover"}
>
{/* Replaces default file panel with our Uppy one. */}
<UppyFilePanel block={block as any} />
<UppyFilePanel blockId={block.id} />
</Components.Generic.Popover.Content>
</Components.Generic.Popover.Root>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const uppy = new Uppy()
});

export function UppyFilePanel(props: FilePanelProps) {
const { block } = props;
const { blockId } = props;
const editor = useBlockNoteEditor();

useEffect(() => {
Expand All @@ -68,7 +68,7 @@ export function UppyFilePanel(props: FilePanelProps) {
url: response.uploadURL,
},
};
editor.updateBlock(block, updateData);
editor.updateBlock(blockId, updateData);

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

// set up dashboard as in https://uppy.io/examples/
return <Dashboard uppy={uppy} width={400} height={500} />;
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@shikijs/types": "3.13.0",
"@tanstack/store": "0.7.7",
"@tiptap/core": "^3.4.3",
"@tiptap/extension-bold": "^3",
"@tiptap/extension-code": "^3",
Expand Down
51 changes: 50 additions & 1 deletion packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { updateBlockTr } from "../api/blockManipulation/commands/updateBlock/upd
import { getBlockInfoFromTransaction } from "../api/getBlockInfoFromPos.js";
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
import "../style.css";
import { ExtensionFactory } from "./managers/extensions/types.js";

/**
* A factory function that returns a BlockNoteExtension
Expand Down Expand Up @@ -1026,15 +1027,52 @@ export class BlockNoteEditor<
ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
key = ext.key(),
): T {
return this._extensionManager.extension(ext, key);
return this._extensionManager.getExtension(key) as any;
}

/**
* Add an extension to the editor
* @param extension The extension to add
* @returns The extension instance
*/
public addExtension(
extension: ReturnType<ExtensionFactory> | ExtensionFactory,
) {
return this._extensionManager.addExtension(extension);
}

public getExtension<
T extends ExtensionFactory | ReturnType<ExtensionFactory> | string,
>(
extension: T,
):
| (T extends ExtensionFactory
? ReturnType<T>
: T extends ReturnType<ExtensionFactory>
? T
: T extends string
? ReturnType<ExtensionFactory>
: never)
| undefined {
return this._extensionManager.getExtension(extension);
}
/**
* Mount the editor to a DOM element.
*
* @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
*/
public mount = (element: HTMLElement) => {
const extensions = this._extensionManager.getExtensions().values();
// TODO can do something similar for input rules
// extensions.filter(e => e.instance.inputRules)

const state = this._tiptapEditor.state.reconfigure({
plugins: this._tiptapEditor.state.plugins.concat(
extensions.flatMap((e) => e.instance.plugins ?? []).toArray(),
),
});
this._tiptapEditor.view.updateState(state);

// TODO: Fix typing for this in a TipTap PR
this._tiptapEditor.mount({ mount: element } as any);
};
Expand Down Expand Up @@ -1573,6 +1611,17 @@ export class BlockNoteEditor<
);
}

public getBlockClientRect(blockId: string): DOMRect | undefined {
const blockElement = this.prosemirrorView.root.querySelector(
`[data-node-type="blockContainer"][data-id="${blockId}"]`,
);
if (!blockElement) {
return;
}

return blockElement.getBoundingClientRect();
}

/**
* A callback function that runs when the editor has been initialized.
*
Expand Down
193 changes: 172 additions & 21 deletions packages/core/src/editor/managers/ExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,173 @@ import { SuggestionMenuProseMirrorPlugin } from "../../extensions/SuggestionMenu
import { TableHandlesProsemirrorPlugin } from "../../extensions/TableHandles/TableHandlesPlugin.js";
import { BlockNoteExtension } from "../BlockNoteExtension.js";
import { BlockNoteEditor } from "../BlockNoteEditor.js";
import { Extension, ExtensionFactory } from "./extensions/types.js";

export class ExtensionManager {
constructor(private editor: BlockNoteEditor) {}
private extensions: Map<
string,
{
instance: Extension;
unmount: () => void;
abortController: AbortController;
}
> = new Map();
private extensionFactories: WeakMap<ExtensionFactory, Extension> =
new WeakMap();
constructor(private editor: BlockNoteEditor) {
editor.onMount(() => {
for (const extension of this.extensions.values()) {
if (extension.instance.init) {
const unmountCallback = extension.instance.init({
dom: editor.prosemirrorView.dom,
root: editor.prosemirrorView.root,
abortController: extension.abortController,
});
extension.unmount = () => {
unmountCallback?.();
extension.abortController.abort();
};
}
}
});

editor.onUnmount(() => {
for (const extension of this.extensions.values()) {
if (extension.unmount) {
extension.unmount();
}
}
});
}

/**
* Shorthand to get a typed extension from the editor, by
* just passing in the extension class.
*
* @param ext - The extension class to get
* @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
* @returns The extension instance
* Get all extensions
*/
public extension<T extends BlockNoteExtension>(
ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
key = ext.key(),
): T {
const extension = this.editor.extensions[key] as T;
if (!extension) {
throw new Error(`Extension ${key} not found`);
public getExtensions() {
return this.extensions;
}

/**
* Add an extension to the editor after initialization
*/
public addExtension<T extends ExtensionFactory | Extension>(
extension: T,
): T extends ExtensionFactory ? ReturnType<T> : T {
if (
typeof extension === "function" &&
this.extensionFactories.has(extension)
) {
return this.extensionFactories.get(extension) as any;
}
return extension;

if (
typeof extension === "object" &&
"key" in extension &&
this.extensions.has(extension.key)
) {
return this.extensions.get(extension.key) as any;
}

const abortController = new AbortController();
let instance: Extension;
if (typeof extension === "function") {
instance = extension(this.editor);
this.extensionFactories.set(extension, instance);
} else {
instance = extension;
}

let unmountCallback: undefined | (() => void) = undefined;

this.extensions.set(instance.key, {
instance,
unmount: () => {
unmountCallback?.();
abortController.abort();
},
abortController,
});

for (const plugin of instance.plugins || []) {
this.editor._tiptapEditor.registerPlugin(plugin);
}

if ("inputRules" in instance) {
// TODO do we need to add new input rules to the editor?
// And other things?
}

if (!this.editor.headless && instance.init) {
unmountCallback =
instance.init({
dom: this.editor.prosemirrorView.dom,
root: this.editor.prosemirrorView.root,
abortController,
}) || undefined;
}

return instance as any;
}

/**
* Get all extensions
* Remove an extension from the editor
* @param extension - The extension to remove
* @returns The extension that was removed
*/
public getExtensions() {
return this.editor.extensions;
public removeExtension<T extends ExtensionFactory | Extension | string>(
extension: T,
): undefined {
let extensionKey: string | undefined;
if (typeof extension === "string") {
extensionKey = extension;
} else if (typeof extension === "function") {
extensionKey = this.extensionFactories.get(extension)?.key;
} else {
extensionKey = extension.key;
}
if (!extensionKey) {
return undefined;
}
const extensionToDelete = this.extensions.get(extensionKey);
if (extensionToDelete) {
if (extensionToDelete.unmount) {
extensionToDelete.unmount();
}
this.extensions.delete(extensionKey);
}
}

/**
* Get a specific extension by key
* Get a specific extension by it's instance
*/
public getExtension(key: string) {
return this.editor.extensions[key];
public getExtension<T extends ExtensionFactory | Extension | string>(
extension: T,
):
| (T extends ExtensionFactory
? ReturnType<T>
: T extends Extension
? T
: T extends string
? Extension
: never)
| undefined {
if (typeof extension === "string") {
if (!this.extensions.has(extension)) {
return undefined;
}
return this.extensions.get(extension) as any;
} else if (typeof extension === "function") {
if (!this.extensionFactories.has(extension)) {
return undefined;
}
return this.extensionFactories.get(extension) as any;
} else if (typeof extension === "object" && "key" in extension) {
if (!this.extensions.has(extension.key)) {
return undefined;
}
return this.extensions.get(extension.key) as any;
}
throw new Error(`Invalid extension type: ${typeof extension}`);
}

/**
Expand All @@ -51,6 +183,25 @@ export class ExtensionManager {
return key in this.editor.extensions;
}

/**
* Shorthand to get a typed extension from the editor, by
* just passing in the extension class.
*
* @param ext - The extension class to get
* @param key - optional, the key of the extension in the extensions object (defaults to the extension name)
* @returns The extension instance
*/
public extension<T extends BlockNoteExtension>(
ext: { new (...args: any[]): T } & typeof BlockNoteExtension,
key = ext.key(),
): T {
const extension = this.editor.extensions[key] as T;
if (!extension) {
throw new Error(`Extension ${key} not found`);
}
return extension;
}

// Plugin getters - these provide access to the core BlockNote plugins

/**
Expand Down
Loading
Loading