Skip to content

Commit 8717e57

Browse files
refactor: clean extensions (#1249)
* clean extensions * Updated test snapshot (was incorrect on main?) --------- Co-authored-by: matthewlipski <[email protected]>
1 parent 98404ae commit 8717e57

File tree

6 files changed

+472
-358
lines changed

6 files changed

+472
-358
lines changed

packages/core/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,6 @@ exports[`Test removeBlocks > Remove all child blocks 1`] = `
308308
},
309309
"id": "table-0",
310310
"props": {
311-
"backgroundColor": "default",
312311
"textColor": "default",
313312
},
314313
"type": "table",

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { EditorOptions, Extension, getSchema } from "@tiptap/core";
1+
import {
2+
AnyExtension,
3+
EditorOptions,
4+
Extension,
5+
getSchema,
6+
Mark,
7+
Node as TipTapNode,
8+
} from "@tiptap/core";
29
import { Node, Schema } from "prosemirror-model";
310
// import "./blocknote.css";
411
import * as Y from "yjs";
@@ -47,9 +54,9 @@ import {
4754
InlineContentSchema,
4855
InlineContentSpecs,
4956
PartialInlineContent,
57+
Styles,
5058
StyleSchema,
5159
StyleSpecs,
52-
Styles,
5360
} from "../schema/index.js";
5461
import { mergeCSSClasses } from "../util/browser.js";
5562
import { NoInfer, UnreachableCaseError } from "../util/typescript.js";
@@ -67,7 +74,6 @@ import {
6774
BlockNoteTipTapEditorOptions,
6875
} from "./BlockNoteTipTapEditor.js";
6976

70-
import { PlaceholderPlugin } from "../extensions/Placeholder/PlaceholderPlugin.js";
7177
import { Dictionary } from "../i18n/dictionary.js";
7278
import { en } from "../i18n/locales/index.js";
7379

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

87+
export type BlockNoteExtension =
88+
| AnyExtension
89+
| {
90+
plugin: Plugin;
91+
};
92+
8393
export type BlockNoteEditorOptions<
8494
BSchema extends BlockSchema,
8595
ISchema extends InlineContentSchema,
@@ -92,7 +102,11 @@ export type BlockNoteEditorOptions<
92102
*/
93103
animations?: boolean;
94104

105+
/**
106+
* Disable internal extensions (based on keys / extension name)
107+
*/
95108
disableExtensions: string[];
109+
96110
/**
97111
* A dictionary object containing translations for the editor.
98112
*/
@@ -173,9 +187,16 @@ export type BlockNoteEditorOptions<
173187
renderCursor?: (user: any) => HTMLElement;
174188
};
175189

176-
// tiptap options, undocumented
190+
/**
191+
* additional tiptap options, undocumented
192+
*/
177193
_tiptapOptions: Partial<EditorOptions>;
178194

195+
/**
196+
* (experimental) add extra prosemirror plugins or tiptap extensions to the editor
197+
*/
198+
_extensions: Record<string, BlockNoteExtension>;
199+
179200
trailingBlock?: boolean;
180201

181202
/**
@@ -213,6 +234,11 @@ export class BlockNoteEditor<
213234
> {
214235
private readonly _pmSchema: Schema;
215236

237+
/**
238+
* extensions that are added to the editor, can be tiptap extensions or prosemirror plugins
239+
*/
240+
public readonly extensions: Record<string, BlockNoteExtension> = {};
241+
216242
/**
217243
* Boolean indicating whether the editor is in headless mode.
218244
* Headless mode means we can use features like importing / exporting blocks,
@@ -355,17 +381,7 @@ export class BlockNoteEditor<
355381
this.inlineContentImplementations = newOptions.schema.inlineContentSpecs;
356382
this.styleImplementations = newOptions.schema.styleSpecs;
357383

358-
this.formattingToolbar = new FormattingToolbarProsemirrorPlugin(this);
359-
this.linkToolbar = new LinkToolbarProsemirrorPlugin(this);
360-
this.sideMenu = new SideMenuProsemirrorPlugin(this);
361-
this.suggestionMenus = new SuggestionMenuProseMirrorPlugin(this);
362-
this.filePanel = new FilePanelProsemirrorPlugin(this as any);
363-
364-
if (checkDefaultBlockTypeInSchema("table", this)) {
365-
this.tableHandles = new TableHandlesProsemirrorPlugin(this as any);
366-
}
367-
368-
const extensions = getBlockNoteExtensions({
384+
this.extensions = getBlockNoteExtensions({
369385
editor: this,
370386
domAttributes: newOptions.domAttributes || {},
371387
blockSpecs: this.schema.blockSpecs,
@@ -375,30 +391,28 @@ export class BlockNoteEditor<
375391
trailingBlock: newOptions.trailingBlock,
376392
disableExtensions: newOptions.disableExtensions,
377393
setIdAttribute: newOptions.setIdAttribute,
394+
animations: newOptions.animations ?? true,
395+
tableHandles: checkDefaultBlockTypeInSchema("table", this),
396+
dropCursor: this.options.dropCursor ?? dropCursor,
397+
placeholders: newOptions.placeholders,
378398
});
379399

380-
const dropCursorPlugin: any = this.options.dropCursor ?? dropCursor;
381-
const blockNoteUIExtension = Extension.create({
382-
name: "BlockNoteUIExtension",
383-
384-
addProseMirrorPlugins: () => {
385-
return [
386-
this.formattingToolbar.plugin,
387-
this.linkToolbar.plugin,
388-
this.sideMenu.plugin,
389-
this.suggestionMenus.plugin,
390-
...(this.filePanel ? [this.filePanel.plugin] : []),
391-
...(this.tableHandles ? [this.tableHandles.plugin] : []),
392-
dropCursorPlugin({ width: 5, color: "#ddeeff", editor: this }),
393-
PlaceholderPlugin(this, newOptions.placeholders),
394-
NodeSelectionKeyboardPlugin(),
395-
...(this.options.animations ?? true
396-
? [PreviousBlockTypePlugin()]
397-
: []),
398-
];
399-
},
400+
// add extensions from _tiptapOptions
401+
(newOptions._tiptapOptions?.extensions || []).forEach((ext) => {
402+
this.extensions[ext.name] = ext;
403+
});
404+
405+
// add extensions from options
406+
Object.entries(newOptions._extensions || {}).forEach(([key, ext]) => {
407+
this.extensions[key] = ext;
400408
});
401-
extensions.push(blockNoteUIExtension);
409+
410+
this.formattingToolbar = this.extensions["formattingToolbar"] as any;
411+
this.linkToolbar = this.extensions["linkToolbar"] as any;
412+
this.sideMenu = this.extensions["sideMenu"] as any;
413+
this.suggestionMenus = this.extensions["suggestionMenus"] as any;
414+
this.filePanel = this.extensions["filePanel"] as any;
415+
this.tableHandles = this.extensions["tableHandles"] as any;
402416

403417
if (newOptions.uploadFile) {
404418
const uploadFile = newOptions.uploadFile;
@@ -449,14 +463,29 @@ export class BlockNoteEditor<
449463
);
450464
}
451465

466+
const tiptapExtensions = [
467+
...Object.entries(this.extensions).map(([key, ext]) => {
468+
if (
469+
ext instanceof Extension ||
470+
ext instanceof TipTapNode ||
471+
ext instanceof Mark
472+
) {
473+
// tiptap extension
474+
return ext;
475+
}
476+
477+
// "blocknote" extensions (prosemirror plugins)
478+
return Extension.create({
479+
name: key,
480+
addProseMirrorPlugins: () => [ext.plugin],
481+
});
482+
}),
483+
];
452484
const tiptapOptions: BlockNoteTipTapEditorOptions = {
453485
...blockNoteTipTapOptions,
454486
...newOptions._tiptapOptions,
455487
content: initialContent,
456-
extensions: [
457-
...(newOptions._tiptapOptions?.extensions || []),
458-
...extensions,
459-
],
488+
extensions: tiptapExtensions,
460489
editorProps: {
461490
...newOptions._tiptapOptions?.editorProps,
462491
attributes: {

packages/core/src/editor/BlockNoteExtensions.ts

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Extension, Extensions, extensions } from "@tiptap/core";
1+
import { AnyExtension, Extension, extensions } from "@tiptap/core";
22

3-
import type { BlockNoteEditor } from "./BlockNoteEditor.js";
3+
import type { BlockNoteEditor, BlockNoteExtension } from "./BlockNoteEditor.js";
44

55
import Collaboration from "@tiptap/extension-collaboration";
66
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
@@ -9,12 +9,22 @@ import { HardBreak } from "@tiptap/extension-hard-break";
99
import { History } from "@tiptap/extension-history";
1010
import { Link } from "@tiptap/extension-link";
1111
import { Text } from "@tiptap/extension-text";
12+
import { Plugin } from "prosemirror-state";
1213
import * as Y from "yjs";
1314
import { createDropFileExtension } from "../api/clipboard/fromClipboard/fileDropExtension.js";
1415
import { createPasteFromClipboardExtension } from "../api/clipboard/fromClipboard/pasteExtension.js";
1516
import { createCopyToClipboardExtension } from "../api/clipboard/toClipboard/copyExtension.js";
1617
import { BackgroundColorExtension } from "../extensions/BackgroundColor/BackgroundColorExtension.js";
18+
import { FilePanelProsemirrorPlugin } from "../extensions/FilePanel/FilePanelPlugin.js";
19+
import { FormattingToolbarProsemirrorPlugin } from "../extensions/FormattingToolbar/FormattingToolbarPlugin.js";
1720
import { KeyboardShortcutsExtension } from "../extensions/KeyboardShortcuts/KeyboardShortcutsExtension.js";
21+
import { LinkToolbarProsemirrorPlugin } from "../extensions/LinkToolbar/LinkToolbarPlugin.js";
22+
import { NodeSelectionKeyboardPlugin } from "../extensions/NodeSelectionKeyboard/NodeSelectionKeyboardPlugin.js";
23+
import { PlaceholderPlugin } from "../extensions/Placeholder/PlaceholderPlugin.js";
24+
import { PreviousBlockTypePlugin } from "../extensions/PreviousBlockType/PreviousBlockTypePlugin.js";
25+
import { SideMenuProsemirrorPlugin } from "../extensions/SideMenu/SideMenuPlugin.js";
26+
import { SuggestionMenuProseMirrorPlugin } from "../extensions/SuggestionMenu/SuggestionPlugin.js";
27+
import { TableHandlesProsemirrorPlugin } from "../extensions/TableHandles/TableHandlesPlugin.js";
1828
import { TextAlignmentExtension } from "../extensions/TextAlignment/TextAlignmentExtension.js";
1929
import { TextColorExtension } from "../extensions/TextColor/TextColorExtension.js";
2030
import { TrailingNode } from "../extensions/TrailingNode/TrailingNodeExtension.js";
@@ -30,14 +40,11 @@ import {
3040
StyleSpecs,
3141
} from "../schema/index.js";
3242

33-
/**
34-
* Get all the Tiptap extensions BlockNote is configured with by default
35-
*/
36-
export const getBlockNoteExtensions = <
43+
type ExtensionOptions<
3744
BSchema extends BlockSchema,
3845
I extends InlineContentSchema,
3946
S extends StyleSchema
40-
>(opts: {
47+
> = {
4148
editor: BlockNoteEditor<BSchema, I, S>;
4249
domAttributes: Partial<BlockNoteDOMAttributes>;
4350
blockSpecs: BlockSpecs;
@@ -56,8 +63,77 @@ export const getBlockNoteExtensions = <
5663
};
5764
disableExtensions: string[] | undefined;
5865
setIdAttribute?: boolean;
59-
}) => {
60-
const ret: Extensions = [
66+
animations: boolean;
67+
tableHandles: boolean;
68+
dropCursor: (opts: any) => Plugin;
69+
placeholders: Record<string | "default", string>;
70+
};
71+
72+
/**
73+
* Get all the Tiptap extensions BlockNote is configured with by default
74+
*/
75+
export const getBlockNoteExtensions = <
76+
BSchema extends BlockSchema,
77+
I extends InlineContentSchema,
78+
S extends StyleSchema
79+
>(
80+
opts: ExtensionOptions<BSchema, I, S>
81+
) => {
82+
const ret: Record<string, BlockNoteExtension> = {};
83+
const tiptapExtensions = getTipTapExtensions(opts);
84+
85+
for (const ext of tiptapExtensions) {
86+
ret[ext.name] = ext;
87+
}
88+
89+
// Note: this is pretty hardcoded and will break when user provides plugins with same keys.
90+
// Define name on plugins instead and not make this a map?
91+
ret["formattingToolbar"] = new FormattingToolbarProsemirrorPlugin(
92+
opts.editor
93+
);
94+
ret["linkToolbar"] = new LinkToolbarProsemirrorPlugin(opts.editor);
95+
ret["sideMenu"] = new SideMenuProsemirrorPlugin(opts.editor);
96+
ret["suggestionMenus"] = new SuggestionMenuProseMirrorPlugin(opts.editor);
97+
ret["filePanel"] = new FilePanelProsemirrorPlugin(opts.editor as any);
98+
ret["placeholder"] = new PlaceholderPlugin(opts.editor, opts.placeholders);
99+
100+
if (opts.animations ?? true) {
101+
ret["animations"] = new PreviousBlockTypePlugin();
102+
}
103+
104+
if (opts.tableHandles) {
105+
ret["tableHandles"] = new TableHandlesProsemirrorPlugin(opts.editor as any);
106+
}
107+
108+
ret["dropCursor"] = {
109+
plugin: opts.dropCursor({
110+
width: 5,
111+
color: "#ddeeff",
112+
editor: opts.editor,
113+
}),
114+
};
115+
116+
ret["nodeSelectionKeyboard"] = new NodeSelectionKeyboardPlugin();
117+
118+
const disableExtensions: string[] = opts.disableExtensions || [];
119+
for (const ext of Object.keys(disableExtensions)) {
120+
delete ret[ext];
121+
}
122+
123+
return ret;
124+
};
125+
126+
/**
127+
* Get all the Tiptap extensions BlockNote is configured with by default
128+
*/
129+
const getTipTapExtensions = <
130+
BSchema extends BlockSchema,
131+
I extends InlineContentSchema,
132+
S extends StyleSchema
133+
>(
134+
opts: ExtensionOptions<BSchema, I, S>
135+
) => {
136+
const tiptapExtensions: AnyExtension[] = [
61137
extensions.ClipboardTextSerializer,
62138
extensions.Commands,
63139
extensions.Editable,
@@ -164,7 +240,7 @@ export const getBlockNoteExtensions = <
164240
];
165241

166242
if (opts.collaboration) {
167-
ret.push(
243+
tiptapExtensions.push(
168244
Collaboration.configure({
169245
fragment: opts.collaboration.fragment,
170246
})
@@ -189,7 +265,7 @@ export const getBlockNoteExtensions = <
189265
cursor.insertBefore(nonbreakingSpace2, null);
190266
return cursor;
191267
};
192-
ret.push(
268+
tiptapExtensions.push(
193269
CollaborationCursor.configure({
194270
user: opts.collaboration.user,
195271
render: opts.collaboration.renderCursor || defaultRender,
@@ -199,9 +275,8 @@ export const getBlockNoteExtensions = <
199275
}
200276
} else {
201277
// disable history extension when collaboration is enabled as Yjs takes care of undo / redo
202-
ret.push(History);
278+
tiptapExtensions.push(History);
203279
}
204280

205-
const disableExtensions: string[] = opts.disableExtensions || [];
206-
return ret.filter((ex) => !disableExtensions.includes(ex.name));
281+
return tiptapExtensions;
207282
};

0 commit comments

Comments
 (0)