Skip to content

Commit

Permalink
refactor: clean extensions (#1249)
Browse files Browse the repository at this point in the history
* clean extensions

* Updated test snapshot (was incorrect on main?)

---------

Co-authored-by: matthewlipski <[email protected]>
  • Loading branch information
YousefED and matthewlipski authored Nov 15, 2024
1 parent 98404ae commit 8717e57
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 358 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,6 @@ exports[`Test removeBlocks > Remove all child blocks 1`] = `
},
"id": "table-0",
"props": {
"backgroundColor": "default",
"textColor": "default",
},
"type": "table",
Expand Down
113 changes: 71 additions & 42 deletions packages/core/src/editor/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { EditorOptions, Extension, getSchema } from "@tiptap/core";
import {
AnyExtension,
EditorOptions,
Extension,
getSchema,
Mark,
Node as TipTapNode,
} from "@tiptap/core";
import { Node, Schema } from "prosemirror-model";
// import "./blocknote.css";
import * as Y from "yjs";
Expand Down Expand Up @@ -47,9 +54,9 @@ import {
InlineContentSchema,
InlineContentSpecs,
PartialInlineContent,
Styles,
StyleSchema,
StyleSpecs,
Styles,
} from "../schema/index.js";
import { mergeCSSClasses } from "../util/browser.js";
import { NoInfer, UnreachableCaseError } from "../util/typescript.js";
Expand All @@ -67,7 +74,6 @@ import {
BlockNoteTipTapEditorOptions,
} from "./BlockNoteTipTapEditor.js";

import { PlaceholderPlugin } from "../extensions/Placeholder/PlaceholderPlugin.js";
import { Dictionary } from "../i18n/dictionary.js";
import { en } from "../i18n/locales/index.js";

Expand All @@ -76,10 +82,14 @@ import { dropCursor } from "prosemirror-dropcursor";
import { createInternalHTMLSerializer } from "../api/exporters/html/internalHTMLSerializer.js";
import { inlineContentToNodes } from "../api/nodeConversions/blockToNode.js";
import { nodeToBlock } from "../api/nodeConversions/nodeToBlock.js";
import { NodeSelectionKeyboardPlugin } from "../extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.js";
import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/PreviousBlockTypePlugin.js";
import "../style.css";

export type BlockNoteExtension =
| AnyExtension
| {
plugin: Plugin;
};

export type BlockNoteEditorOptions<
BSchema extends BlockSchema,
ISchema extends InlineContentSchema,
Expand All @@ -92,7 +102,11 @@ export type BlockNoteEditorOptions<
*/
animations?: boolean;

/**
* Disable internal extensions (based on keys / extension name)
*/
disableExtensions: string[];

/**
* A dictionary object containing translations for the editor.
*/
Expand Down Expand Up @@ -173,9 +187,16 @@ export type BlockNoteEditorOptions<
renderCursor?: (user: any) => HTMLElement;
};

// tiptap options, undocumented
/**
* additional tiptap options, undocumented
*/
_tiptapOptions: Partial<EditorOptions>;

/**
* (experimental) add extra prosemirror plugins or tiptap extensions to the editor
*/
_extensions: Record<string, BlockNoteExtension>;

trailingBlock?: boolean;

/**
Expand Down Expand Up @@ -213,6 +234,11 @@ export class BlockNoteEditor<
> {
private readonly _pmSchema: Schema;

/**
* extensions that are added to the editor, can be tiptap extensions or prosemirror plugins
*/
public readonly extensions: Record<string, BlockNoteExtension> = {};

/**
* Boolean indicating whether the editor is in headless mode.
* Headless mode means we can use features like importing / exporting blocks,
Expand Down Expand Up @@ -355,17 +381,7 @@ export class BlockNoteEditor<
this.inlineContentImplementations = newOptions.schema.inlineContentSpecs;
this.styleImplementations = newOptions.schema.styleSpecs;

this.formattingToolbar = new FormattingToolbarProsemirrorPlugin(this);
this.linkToolbar = new LinkToolbarProsemirrorPlugin(this);
this.sideMenu = new SideMenuProsemirrorPlugin(this);
this.suggestionMenus = new SuggestionMenuProseMirrorPlugin(this);
this.filePanel = new FilePanelProsemirrorPlugin(this as any);

if (checkDefaultBlockTypeInSchema("table", this)) {
this.tableHandles = new TableHandlesProsemirrorPlugin(this as any);
}

const extensions = getBlockNoteExtensions({
this.extensions = getBlockNoteExtensions({
editor: this,
domAttributes: newOptions.domAttributes || {},
blockSpecs: this.schema.blockSpecs,
Expand All @@ -375,30 +391,28 @@ export class BlockNoteEditor<
trailingBlock: newOptions.trailingBlock,
disableExtensions: newOptions.disableExtensions,
setIdAttribute: newOptions.setIdAttribute,
animations: newOptions.animations ?? true,
tableHandles: checkDefaultBlockTypeInSchema("table", this),
dropCursor: this.options.dropCursor ?? dropCursor,
placeholders: newOptions.placeholders,
});

const dropCursorPlugin: any = this.options.dropCursor ?? dropCursor;
const blockNoteUIExtension = Extension.create({
name: "BlockNoteUIExtension",

addProseMirrorPlugins: () => {
return [
this.formattingToolbar.plugin,
this.linkToolbar.plugin,
this.sideMenu.plugin,
this.suggestionMenus.plugin,
...(this.filePanel ? [this.filePanel.plugin] : []),
...(this.tableHandles ? [this.tableHandles.plugin] : []),
dropCursorPlugin({ width: 5, color: "#ddeeff", editor: this }),
PlaceholderPlugin(this, newOptions.placeholders),
NodeSelectionKeyboardPlugin(),
...(this.options.animations ?? true
? [PreviousBlockTypePlugin()]
: []),
];
},
// add extensions from _tiptapOptions
(newOptions._tiptapOptions?.extensions || []).forEach((ext) => {
this.extensions[ext.name] = ext;
});

// add extensions from options
Object.entries(newOptions._extensions || {}).forEach(([key, ext]) => {
this.extensions[key] = ext;
});
extensions.push(blockNoteUIExtension);

this.formattingToolbar = this.extensions["formattingToolbar"] as any;
this.linkToolbar = this.extensions["linkToolbar"] as any;
this.sideMenu = this.extensions["sideMenu"] as any;
this.suggestionMenus = this.extensions["suggestionMenus"] as any;
this.filePanel = this.extensions["filePanel"] as any;
this.tableHandles = this.extensions["tableHandles"] as any;

if (newOptions.uploadFile) {
const uploadFile = newOptions.uploadFile;
Expand Down Expand Up @@ -449,14 +463,29 @@ export class BlockNoteEditor<
);
}

const tiptapExtensions = [
...Object.entries(this.extensions).map(([key, ext]) => {
if (
ext instanceof Extension ||
ext instanceof TipTapNode ||
ext instanceof Mark
) {
// tiptap extension
return ext;
}

// "blocknote" extensions (prosemirror plugins)
return Extension.create({
name: key,
addProseMirrorPlugins: () => [ext.plugin],
});
}),
];
const tiptapOptions: BlockNoteTipTapEditorOptions = {
...blockNoteTipTapOptions,
...newOptions._tiptapOptions,
content: initialContent,
extensions: [
...(newOptions._tiptapOptions?.extensions || []),
...extensions,
],
extensions: tiptapExtensions,
editorProps: {
...newOptions._tiptapOptions?.editorProps,
attributes: {
Expand Down
103 changes: 89 additions & 14 deletions packages/core/src/editor/BlockNoteExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Extension, Extensions, extensions } from "@tiptap/core";
import { AnyExtension, Extension, extensions } from "@tiptap/core";

import type { BlockNoteEditor } from "./BlockNoteEditor.js";
import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";

import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
Expand All @@ -9,12 +9,22 @@ import { HardBreak } from "@tiptap/extension-hard-break";
import { History } from "@tiptap/extension-history";
import { Link } from "@tiptap/extension-link";
import { Text } from "@tiptap/extension-text";
import { Plugin } from "prosemirror-state";
import * as Y from "yjs";
import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension.js";
import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension.js";
import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js";
import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js";
import { KeyboardShortcutsExtension } from "../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js";
import { LinkToolbarProsemirrorPlugin } from "../extensions/LinkToolbar/LinkToolbarPlugin.js";
import { NodeSelectionKeyboardPlugin } from "../extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.js";
import { PlaceholderPlugin } from "../extensions/Placeholder/PlaceholderPlugin.js";
import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/PreviousBlockTypePlugin.js";
import { SideMenuProsemirrorPlugin } from "../extensions/SideMenu/SideMenuPlugin.js";
import { SuggestionMenuProseMirrorPlugin } from "../extensions/SuggestionMenu/SuggestionPlugin.js";
import { TableHandlesProsemirrorPlugin } from "../extensions/TableHandles/TableHandlesPlugin.js";
import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension.js";
import { TextColorExtension } from "../extensions/TextColor/TextColorExtension.js";
import { TrailingNode } from "../extensions/TrailingNode/TrailingNodeExtension.js";
Expand All @@ -30,14 +40,11 @@ import {
StyleSpecs,
} from "../schema/index.js";

/**
* Get all the Tiptap extensions BlockNote is configured with by default
*/
export const getBlockNoteExtensions = <
type ExtensionOptions<
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(opts: {
> = {
editor: BlockNoteEditor<BSchema, I, S>;
domAttributes: Partial<BlockNoteDOMAttributes>;
blockSpecs: BlockSpecs;
Expand All @@ -56,8 +63,77 @@ export const getBlockNoteExtensions = <
};
disableExtensions: string[] | undefined;
setIdAttribute?: boolean;
}) => {
const ret: Extensions = [
animations: boolean;
tableHandles: boolean;
dropCursor: (opts: any) => Plugin;
placeholders: Record<string | "default", string>;
};

/**
* Get all the Tiptap extensions BlockNote is configured with by default
*/
export const getBlockNoteExtensions = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
opts: ExtensionOptions<BSchema, I, S>
) => {
const ret: Record<string, BlockNoteExtension> = {};
const tiptapExtensions = getTipTapExtensions(opts);

for (const ext of tiptapExtensions) {
ret[ext.name] = ext;
}

// Note: this is pretty hardcoded and will break when user provides plugins with same keys.
// Define name on plugins instead and not make this a map?
ret["formattingToolbar"] = new FormattingToolbarProsemirrorPlugin(
opts.editor
);
ret["linkToolbar"] = new LinkToolbarProsemirrorPlugin(opts.editor);
ret["sideMenu"] = new SideMenuProsemirrorPlugin(opts.editor);
ret["suggestionMenus"] = new SuggestionMenuProseMirrorPlugin(opts.editor);
ret["filePanel"] = new FilePanelProsemirrorPlugin(opts.editor as any);
ret["placeholder"] = new PlaceholderPlugin(opts.editor, opts.placeholders);

if (opts.animations ?? true) {
ret["animations"] = new PreviousBlockTypePlugin();
}

if (opts.tableHandles) {
ret["tableHandles"] = new TableHandlesProsemirrorPlugin(opts.editor as any);
}

ret["dropCursor"] = {
plugin: opts.dropCursor({
width: 5,
color: "#ddeeff",
editor: opts.editor,
}),
};

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

const disableExtensions: string[] = opts.disableExtensions || [];
for (const ext of Object.keys(disableExtensions)) {
delete ret[ext];
}

return ret;
};

/**
* Get all the Tiptap extensions BlockNote is configured with by default
*/
const getTipTapExtensions = <
BSchema extends BlockSchema,
I extends InlineContentSchema,
S extends StyleSchema
>(
opts: ExtensionOptions<BSchema, I, S>
) => {
const tiptapExtensions: AnyExtension[] = [
extensions.ClipboardTextSerializer,
extensions.Commands,
extensions.Editable,
Expand Down Expand Up @@ -164,7 +240,7 @@ export const getBlockNoteExtensions = <
];

if (opts.collaboration) {
ret.push(
tiptapExtensions.push(
Collaboration.configure({
fragment: opts.collaboration.fragment,
})
Expand All @@ -189,7 +265,7 @@ export const getBlockNoteExtensions = <
cursor.insertBefore(nonbreakingSpace2, null);
return cursor;
};
ret.push(
tiptapExtensions.push(
CollaborationCursor.configure({
user: opts.collaboration.user,
render: opts.collaboration.renderCursor || defaultRender,
Expand All @@ -199,9 +275,8 @@ export const getBlockNoteExtensions = <
}
} else {
// disable history extension when collaboration is enabled as Yjs takes care of undo / redo
ret.push(History);
tiptapExtensions.push(History);
}

const disableExtensions: string[] = opts.disableExtensions || [];
return ret.filter((ex) => !disableExtensions.includes(ex.name));
return tiptapExtensions;
};
Loading

0 comments on commit 8717e57

Please sign in to comment.