Skip to content
Merged
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
36 changes: 26 additions & 10 deletions packages/core/src/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
blocksToMarkdown,
markdownToBlocks,
} from "./api/formatConversions/formatConversions";
import { nodeToBlock } from "./api/nodeConversions/nodeConversions";
import {
blockToNode,
nodeToBlock,
} from "./api/nodeConversions/nodeConversions";
import { getNodeById } from "./api/util/nodeUtil";
import styles from "./editor.module.css";
import {
Expand Down Expand Up @@ -44,6 +47,7 @@ import { SideMenuProsemirrorPlugin } from "./extensions/SideMenu/SideMenuPlugin"
import { BaseSlashMenuItem } from "./extensions/SlashMenu/BaseSlashMenuItem";
import { SlashMenuProsemirrorPlugin } from "./extensions/SlashMenu/SlashMenuPlugin";
import { getDefaultSlashMenuItems } from "./extensions/SlashMenu/defaultSlashMenuItems";
import { UniqueID } from "./extensions/UniqueID/UniqueID";

export type BlockNoteEditorOptions<BSchema extends BlockSchema> = {
// TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them.
Expand Down Expand Up @@ -201,22 +205,34 @@ export class BlockNoteEditor<BSchema extends BlockSchema = DefaultBlockSchema> {

this.schema = newOptions.blockSchema;

const initialContent = newOptions.initialContent || [
{
type: "paragraph",
id: UniqueID.options.generateID(),
},
];

const tiptapOptions: EditorOptions = {
// TODO: This approach to setting initial content is "cleaner" but requires the PM editor schema, which is only
// created after initializing the TipTap editor. Not sure it's feasible.
// content:
// options.initialContent &&
// options.initialContent.map((block) =>
// blockToNode(block, this._tiptapEditor.schema).toJSON()
// ),
...blockNoteTipTapOptions,
...newOptions._tiptapOptions,
onCreate: () => {
newOptions.onEditorReady?.(this);
newOptions.initialContent &&
this.replaceBlocks(this.topLevelBlocks, newOptions.initialContent);
this.ready = true;
},
onBeforeCreate(editor) {
// we have to set the initial content here, because now we can use the editor schema
// which has been created at this point
const schema = editor.editor.schema;
const ic = initialContent.map((block) => blockToNode(block, schema));

const root = schema.node(
"doc",
undefined,
schema.node("blockGroup", undefined, ic)
);
// override the initialcontent
editor.editor.options.content = root.toJSON();
},
onUpdate: () => {
// This seems to be necessary due to a bug in TipTap:
// https://github.com/ueberdosis/tiptap/issues/2583
Expand Down
59 changes: 29 additions & 30 deletions packages/core/src/extensions/UniqueID/UniqueID.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
combineTransactionSteps,
Extension,
findChildren,
findChildrenInRange,
getChangedRanges,
} from "@tiptap/core";
Expand Down Expand Up @@ -87,35 +86,35 @@ const UniqueID = Extension.create({
];
},
// check initial content for missing ids
onCreate() {
// Don’t do this when the collaboration extension is active
// because this may update the content, so Y.js tries to merge these changes.
// This leads to empty block nodes.
// See: https://github.com/ueberdosis/tiptap/issues/2400
if (
this.editor.extensionManager.extensions.find(
(extension) => extension.name === "collaboration"
)
) {
return;
}
const { view, state } = this.editor;
const { tr, doc } = state;
const { types, attributeName, generateID } = this.options;
const nodesWithoutId = findChildren(doc, (node) => {
return (
types.includes(node.type.name) && node.attrs[attributeName] === null
);
});
nodesWithoutId.forEach(({ node, pos }) => {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
[attributeName]: generateID(),
});
});
tr.setMeta("addToHistory", false);
view.dispatch(tr);
},
// onCreate() {
// // Don’t do this when the collaboration extension is active
// // because this may update the content, so Y.js tries to merge these changes.
// // This leads to empty block nodes.
// // See: https://github.com/ueberdosis/tiptap/issues/2400
// if (
// this.editor.extensionManager.extensions.find(
// (extension) => extension.name === "collaboration"
// )
// ) {
// return;
// }
// const { view, state } = this.editor;
// const { tr, doc } = state;
// const { types, attributeName, generateID } = this.options;
// const nodesWithoutId = findChildren(doc, (node) => {
// return (
// types.includes(node.type.name) && node.attrs[attributeName] === null
// );
// });
// nodesWithoutId.forEach(({ node, pos }) => {
// tr.setNodeMarkup(pos, undefined, {
// ...node.attrs,
// [attributeName]: generateID(),
// });
// });
// tr.setMeta("addToHistory", false);
// view.dispatch(tr);
// },
addProseMirrorPlugins() {
let dragSourceElement: any = null;
let transformPasted = false;
Expand Down
39 changes: 10 additions & 29 deletions packages/react/src/BlockNoteView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BlockNoteEditor, BlockSchema } from "@blocknote/core";
import { MantineProvider } from "@mantine/core";
import { EditorContent } from "@tiptap/react";
import { ReactNode, useEffect, useState } from "react";
import { HTMLAttributes, ReactNode } from "react";
import { getBlockNoteTheme } from "./BlockNoteTheme";
import { FormattingToolbarPositioner } from "./FormattingToolbar/components/FormattingToolbarPositioner";
import { HyperlinkToolbarPositioner } from "./HyperlinkToolbar/components/HyperlinkToolbarPositioner";
Expand All @@ -12,40 +12,21 @@ export function BlockNoteView<BSchema extends BlockSchema>(
props: {
editor: BlockNoteEditor<BSchema>;
children?: ReactNode;
} & React.HTMLAttributes<HTMLDivElement>
} & HTMLAttributes<HTMLDivElement>
) {
const { editor, children, ...rest } = props;
const [ready, setReady] = useState(false);

useEffect(() => {
function checkReady() {
if (!props.editor.ready) {
window.setTimeout(checkReady, 100);
} else {
setReady(true);
}
}
checkReady();

// TODO: Shouldn't this work with props.editor.ready in deps? It fails in
// tests so it doesn't seem to.
// if (props.editor.ready) {
// setReady(true);
// }
}, [props.editor.ready]);

return (
<MantineProvider theme={getBlockNoteTheme()}>
<EditorContent editor={props.editor?._tiptapEditor} {...rest}>
{ready &&
(props.children || (
<>
<FormattingToolbarPositioner editor={props.editor} />
<HyperlinkToolbarPositioner editor={props.editor} />
<SlashMenuPositioner editor={props.editor} />
<SideMenuPositioner editor={props.editor} />
</>
))}
{props.children || (
<>
<FormattingToolbarPositioner editor={props.editor} />
<HyperlinkToolbarPositioner editor={props.editor} />
<SlashMenuPositioner editor={props.editor} />
<SideMenuPositioner editor={props.editor} />
</>
)}
</EditorContent>
</MantineProvider>
);
Expand Down
7 changes: 4 additions & 3 deletions packages/react/src/hooks/useBlockNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ export const useBlockNote = <BSchema extends BlockSchema = DefaultBlockSchema>(
options: Partial<BlockNoteEditorOptions<BSchema>> = {},
deps: DependencyList = []
): BlockNoteEditor<BSchema> => {
const editorRef = useRef<BlockNoteEditor<BSchema>>(initEditor(options));
const editorRef = useRef<BlockNoteEditor<BSchema>>();

useMemo(() => {
const ret = useMemo(() => {
if (editorRef.current) {
editorRef.current._tiptapEditor.destroy();
}

editorRef.current = initEditor(options);
return editorRef.current;
}, [deps, options]); //eslint-disable-line react-hooks/exhaustive-deps

return editorRef.current;
return ret;
};