Skip to content

Commit c43b119

Browse files
committed
feat: preliminary support for tiptap v3
1 parent 37f0765 commit c43b119

File tree

22 files changed

+81
-169
lines changed

22 files changed

+81
-169
lines changed

packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@tiptap/extension-collaboration-cursor": "^2.11.5",
7171
"@tiptap/extension-gapcursor": "^2.11.5",
7272
"@tiptap/extension-hard-break": "^2.11.5",
73-
"@tiptap/extension-history": "^2.11.5",
73+
"@tiptap/extension-undo-redo": "^2.11.5",
7474
"@tiptap/extension-horizontal-rule": "^2.11.5",
7575
"@tiptap/extension-italic": "^2.11.5",
7676
"@tiptap/extension-link": "^2.11.5",

packages/core/src/api/blockManipulation/setupTestEnv.ts

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export function setupTestEnv() {
1313
});
1414

1515
afterAll(() => {
16-
editor.mount(undefined);
1716
editor._tiptapEditor.destroy();
1817
editor = undefined as any;
1918
});

packages/core/src/api/clipboard/clipboardExternal.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ describe("Test external clipboard HTML", () => {
6969
});
7070

7171
afterAll(() => {
72-
editor.mount(undefined);
7372
editor._tiptapEditor.destroy();
7473
editor = undefined as any;
7574

packages/core/src/api/clipboard/clipboardInternal.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,6 @@ describe("Test ProseMirror selection clipboard HTML", () => {
283283
});
284284

285285
afterAll(() => {
286-
editor.mount(undefined);
287286
editor._tiptapEditor.destroy();
288287
editor = undefined as any;
289288

packages/core/src/api/exporters/html/htmlConversion.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ describe("Test HTML conversion", () => {
8686
});
8787

8888
afterEach(() => {
89-
editor.mount(undefined);
9089
editor._tiptapEditor.destroy();
9190
editor = undefined as any;
9291

packages/core/src/api/exporters/markdown/markdownExporter.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ describe("markdownExporter", () => {
6262
});
6363

6464
afterEach(() => {
65-
editor.mount(undefined);
6665
editor._tiptapEditor.destroy();
6766
editor = undefined as any;
6867

packages/core/src/api/nodeConversions/nodeConversions.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ describe("Test BlockNote-Prosemirror conversion", () => {
6464
});
6565

6666
afterEach(() => {
67-
editor.mount(undefined);
6867
editor._tiptapEditor.destroy();
6968
editor = undefined as any;
7069

packages/core/src/api/parsers/html/parseHTML.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async function parseHTMLAndCompareSnapshots(
5555
pastedBlocks.pop(); // trailing paragraph
5656
expect(pastedBlocks).toStrictEqual(blocks);
5757

58-
editor.mount(undefined);
58+
editor.unmount();
5959
}
6060

6161
describe("Parse HTML", () => {

packages/core/src/api/parsers/markdown/parseMarkdown.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async function parseMarkdownAndCompareSnapshots(
2727
pastedSnapshotPath
2828
);
2929

30-
editor.mount(undefined);
30+
editor.unmount();
3131
}
3232

3333
describe("Parse Markdown", () => {

packages/core/src/blocks/CodeBlockContent/CodeBlockContent.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ const CodeBlockContent = createStronglyTypedTiptapNode({
163163
const language = (event.target as HTMLSelectElement).value;
164164

165165
editor.commands.command(({ tr }) => {
166-
tr.setNodeAttribute(getPos(), "language", language);
166+
const pos = getPos();
167+
if (!pos) {
168+
return false;
169+
}
170+
tr.setNodeAttribute(pos, "language", language);
167171

168172
return true;
169173
});

packages/core/src/blocks/ListItemBlockContent/CheckListItemBlockContent/CheckListItemBlockContent.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,13 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
227227
return;
228228
}
229229

230+
const pos = getPos();
231+
230232
// TODO: test
231-
if (typeof getPos !== "boolean") {
233+
if (pos) {
232234
const beforeBlockContainerPos = getNearestBlockPos(
233235
editor.state.doc,
234-
getPos()
236+
pos
235237
);
236238

237239
if (beforeBlockContainerPos.node.type.name !== "blockContainer") {
@@ -266,12 +268,14 @@ const checkListItemBlockContent = createStronglyTypedTiptapNode({
266268
this.options.domAttributes?.inlineContent || {}
267269
);
268270

269-
if (typeof getPos !== "boolean") {
271+
const pos = getPos();
272+
273+
if (pos) {
270274
// Since `node` is a blockContent node, we have to get the block ID from
271275
// the parent blockContainer node. This means we can't add the label in
272276
// `renderHTML` as we can't use `getPos` and therefore can't get the
273277
// parent blockContainer node.
274-
const blockID = this.editor.state.doc.resolve(getPos()).node().attrs.id;
278+
const blockID = this.editor.state.doc.resolve(pos).node().attrs.id;
275279
const label = "label-" + blockID;
276280
checkbox.setAttribute("aria-labelledby", label);
277281
contentDOM.id = label;

packages/core/src/editor/BlockNoteEditor.ts

+21-17
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,14 @@ export class BlockNoteEditor<
612612

613613
const tiptapExtensions = [
614614
...Object.entries(this.extensions).map(([key, ext]) => {
615+
if ("plugin" in ext) {
616+
// "blocknote" extensions (prosemirror plugins)
617+
return Extension.create({
618+
name: key,
619+
addProseMirrorPlugins: () => [ext.plugin],
620+
});
621+
}
622+
615623
if (
616624
ext instanceof Extension ||
617625
ext instanceof TipTapNode ||
@@ -621,17 +629,9 @@ export class BlockNoteEditor<
621629
return ext;
622630
}
623631

624-
if (!ext.plugin) {
625-
throw new Error(
626-
"Extension should either be a TipTap extension or a ProseMirror plugin in a plugin property"
627-
);
628-
}
629-
630-
// "blocknote" extensions (prosemirror plugins)
631-
return Extension.create({
632-
name: key,
633-
addProseMirrorPlugins: () => [ext.plugin],
634-
});
632+
throw new Error(
633+
"Extension should either be a TipTap extension or a ProseMirror plugin in a plugin property"
634+
);
635635
}),
636636
];
637637
const tiptapOptions: BlockNoteTipTapEditorOptions = {
@@ -680,15 +680,19 @@ export class BlockNoteEditor<
680680
};
681681

682682
/**
683-
* Mount the editor to a parent DOM element. Call mount(undefined) to clean up
683+
* Mount the editor to a parent DOM element.
684684
*
685685
* @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
686686
*/
687-
public mount = (
688-
parentElement?: HTMLElement | null,
689-
contentComponent?: any
690-
) => {
691-
this._tiptapEditor.mount(parentElement, contentComponent);
687+
public mount = (parentElement: HTMLElement) => {
688+
this._tiptapEditor.mount(parentElement);
689+
};
690+
691+
/**
692+
* Unmount the editor from the DOM element it is bound to
693+
*/
694+
public unmount = () => {
695+
this._tiptapEditor.unmount();
692696
};
693697

694698
/**

packages/core/src/editor/BlockNoteExtensions.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { AnyExtension, Extension, extensions } from "@tiptap/core";
1+
import { /*AnyExtension,*/ Extension, extensions } from "@tiptap/core";
22
import { Gapcursor } from "@tiptap/extension-gapcursor";
33
import { HardBreak } from "@tiptap/extension-hard-break";
4-
import { History } from "@tiptap/extension-history";
4+
import { UndoRedo } from "@tiptap/extension-undo-redo";
55
import { Link } from "@tiptap/extension-link";
66
import { Text } from "@tiptap/extension-text";
77
import { Plugin } from "prosemirror-state";
@@ -163,7 +163,8 @@ const getTipTapExtensions = <
163163
>(
164164
opts: ExtensionOptions<BSchema, I, S>
165165
) => {
166-
const tiptapExtensions: AnyExtension[] = [
166+
// TODO just for now
167+
const tiptapExtensions: any[] = [
167168
extensions.ClipboardTextSerializer,
168169
extensions.Commands,
169170
extensions.Editable,
@@ -275,7 +276,7 @@ const getTipTapExtensions = <
275276
tiptapExtensions.push(...createCollaborationExtensions(opts.collaboration));
276277
} else {
277278
// disable history extension when collaboration is enabled as Yjs takes care of undo / redo
278-
tiptapExtensions.push(History);
279+
tiptapExtensions.push(UndoRedo);
279280
}
280281

281282
return tiptapExtensions;

packages/core/src/editor/BlockNoteTipTapEditor.ts

+10-122
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { Editor as TiptapEditor } from "@tiptap/core";
44

55
import { Node } from "@tiptap/pm/model";
66

7-
import { EditorView } from "@tiptap/pm/view";
8-
97
import { EditorState, Transaction } from "@tiptap/pm/state";
108
import { blockToNode } from "../api/nodeConversions/blockToNode.js";
119
import { PartialBlock } from "../blocks/defaultBlocks.js";
@@ -22,46 +20,20 @@ export type BlockNoteTipTapEditorOptions = Partial<
2220
* the creation of the view from the constructor.
2321
*/
2422
export class BlockNoteTipTapEditor extends TiptapEditor {
25-
private _state: EditorState;
26-
2723
public static create = (
2824
options: BlockNoteTipTapEditorOptions,
2925
styleSchema: StyleSchema
3026
) => {
31-
// because we separate the constructor from the creation of the view,
32-
// we need to patch setTimeout to prevent this code from having any effect:
33-
// https://github.com/ueberdosis/tiptap/blob/45bac803283446795ad1b03f43d3746fa54a68ff/packages/core/src/Editor.ts#L117
34-
const oldSetTimeout = globalThis?.window?.setTimeout;
35-
if (typeof globalThis?.window?.setTimeout !== "undefined") {
36-
globalThis.window.setTimeout = (() => {
37-
return 0;
38-
}) as any;
39-
}
40-
try {
41-
return new BlockNoteTipTapEditor(options, styleSchema);
42-
} finally {
43-
if (oldSetTimeout) {
44-
globalThis.window.setTimeout = oldSetTimeout;
45-
}
46-
}
27+
return new BlockNoteTipTapEditor(options, styleSchema);
4728
};
4829

4930
protected constructor(
5031
options: BlockNoteTipTapEditorOptions,
5132
styleSchema: StyleSchema
5233
) {
53-
// possible fix for next.js server side rendering
54-
// const d = globalThis.document;
55-
// const w = globalThis.window;
56-
// if (!globalThis.document) {
57-
// globalThis.document = {
58-
// createElement: () => {},
59-
// };
60-
// }
61-
6234
// options.injectCSS = false
6335

64-
super({ ...options, content: undefined });
36+
super({ ...options, content: undefined, element: null });
6537
// try {
6638
// globalThis.window = w;
6739
// } catch(e) {}
@@ -120,100 +92,16 @@ export class BlockNoteTipTapEditor extends TiptapEditor {
12092
);
12193
}
12294

123-
// Create state immediately, so that it's available independently from the View,
124-
// the way Prosemirror "intends it to be". This also makes sure that we can access
125-
// the state before the view is created / mounted.
126-
this._state = EditorState.create({
127-
doc,
128-
schema: this.schema,
129-
// selection: selection || undefined,
130-
});
131-
}
132-
133-
get state() {
134-
if (this.view) {
135-
this._state = this.view.state;
136-
}
137-
return this._state;
95+
// Leverage the fact that we know the view is not created yet, and quickly set the initial state
96+
this.view.updateState(
97+
EditorState.create({
98+
doc,
99+
schema: this.schema,
100+
})
101+
);
138102
}
139103

140104
dispatch(tr: Transaction) {
141-
if (this.view) {
142-
this.view.dispatch(tr);
143-
} else {
144-
// before view has been initialized
145-
this._state = this.state.apply(tr);
146-
}
147-
}
148-
149-
/**
150-
* Replace the default `createView` method with a custom one - which we call on mount
151-
*/
152-
private createViewAlternative(contentComponent?: any) {
153-
(this as any).contentComponent = contentComponent;
154-
155-
const markViews: any = {};
156-
this.extensionManager.extensions.forEach((extension) => {
157-
if (extension.type === "mark" && extension.config.addMarkView) {
158-
// Note: migrate to using `addMarkView` from tiptap as soon as this lands
159-
// (currently tiptap doesn't support markviews)
160-
markViews[extension.name] = extension.config.addMarkView;
161-
}
162-
});
163-
164-
this.view = new EditorView(
165-
{ mount: this.options.element as any }, // use mount option so that we reuse the existing element instead of creating a new one
166-
{
167-
...this.options.editorProps,
168-
// @ts-ignore
169-
dispatchTransaction: this.dispatchTransaction.bind(this),
170-
state: this.state,
171-
markViews,
172-
}
173-
);
174-
175-
// `editor.view` is not yet available at this time.
176-
// Therefore we will add all plugins and node views directly afterwards.
177-
const newState = this.state.reconfigure({
178-
plugins: this.extensionManager.plugins,
179-
});
180-
181-
this.view.updateState(newState);
182-
183-
this.createNodeViews();
184-
185-
// emit the created event, call here manually because we blocked the default call in the constructor
186-
// (https://github.com/ueberdosis/tiptap/blob/45bac803283446795ad1b03f43d3746fa54a68ff/packages/core/src/Editor.ts#L117)
187-
this.commands.focus(
188-
this.options.autofocus ||
189-
this.options.element.getAttribute("data-bn-autofocus") === "true",
190-
{ scrollIntoView: false }
191-
);
192-
this.emit("create", { editor: this });
193-
this.isInitialized = true;
105+
this.view.dispatch(tr);
194106
}
195-
196-
/**
197-
* Mounts / unmounts the editor to a dom element
198-
*
199-
* @param element DOM element to mount to, ur null / undefined to destroy
200-
*/
201-
public mount = (element?: HTMLElement | null, contentComponent?: any) => {
202-
if (!element) {
203-
this.destroy();
204-
} else {
205-
this.options.element = element;
206-
this.createViewAlternative(contentComponent);
207-
}
208-
};
209107
}
210-
211-
(BlockNoteTipTapEditor.prototype as any).createView = function () {
212-
// no-op
213-
// Disable default call to `createView` in the Editor constructor.
214-
// We should call `createView` manually only when a DOM element is available
215-
216-
// additional fix because onPaste and onDrop depend on installing plugins in constructor which we don't support
217-
// (note: can probably be removed after tiptap upgrade fixed in 2.8.0)
218-
this.options.onPaste = this.options.onDrop = undefined;
219-
};

0 commit comments

Comments
 (0)