Skip to content

wip: custom blocks typing and API #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
112 changes: 88 additions & 24 deletions packages/core/src/BlockNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,41 @@ import { getNodeById } from "./api/util/nodeUtil";
import { getBlockNoteExtensions, UiFactories } from "./BlockNoteExtensions";
import styles from "./editor.module.css";
import {
Block,
BlockIdentifier,
PartialBlock,
BlockSpec,
BlockTemplate,
PartialBlockTemplate,
} from "./extensions/Blocks/api/blockTypes";
import {
MouseCursorPosition,
TextCursorPosition,
} from "./extensions/Blocks/api/cursorPositionTypes";
import {
defaultBlocks,
DefaultBlockTypes,
} from "./extensions/Blocks/api/defaultBlocks";
import {
ColorStyle,
Styles,
ToggledStyle,
} from "./extensions/Blocks/api/inlineContentTypes";
import {
MouseCursorPosition,
TextCursorPosition,
} from "./extensions/Blocks/api/cursorPositionTypes";
import { Selection } from "./extensions/Blocks/api/selectionTypes";
import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos";
import {
BaseSlashMenuItem,
defaultSlashMenuItems,
} from "./extensions/SlashMenu";

export type BlockNoteEditorOptions = {
// We need to separate WithChildren, otherwise we get issues with recursive types
// maybe worth a revisit before merging
type WithChildren<Block extends BlockTemplate<any, any>> = Block & {
children: WithChildren<Block>[];
};

export type BlockNoteEditorOptions<
BareBlock extends BlockTemplate<any, any>,
Block extends BareBlock = WithChildren<BareBlock>
> = {
// TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them.
enableBlockNoteExtensions: boolean;
disableHistoryExtension: boolean;
Expand Down Expand Up @@ -69,16 +83,16 @@ export type BlockNoteEditorOptions = {
/**
* A callback function that runs when the editor is ready to be used.
*/
onEditorReady: (editor: BlockNoteEditor) => void;
onEditorReady: (editor: BlockNoteEditor<Block>) => void;
/**
* A callback function that runs whenever the editor's contents change.
*/
onEditorContentChange: (editor: BlockNoteEditor) => void;
onEditorContentChange: (editor: BlockNoteEditor<Block>) => void;
/**
* A callback function that runs whenever the text cursor position changes.
*/
onTextCursorPositionChange: (editor: BlockNoteEditor) => void;
initialContent: PartialBlock[];
onTextCursorPositionChange: (editor: BlockNoteEditor<Block>) => void;
initialContent: PartialBlockTemplate<Block>[];

/**
* Use default BlockNote font and reset the styles of <p> <li> <h1> elements etc., that are used in BlockNote.
Expand All @@ -87,6 +101,10 @@ export type BlockNoteEditorOptions = {
*/
defaultStyles: boolean;

/**
* A list of block types that should be available in the editor.
*/
blocks: BlockSpec[]; // TODO, type this so that it matches <Block>
// tiptap options, undocumented
_tiptapOptions: any;
};
Expand All @@ -97,10 +115,15 @@ const blockNoteTipTapOptions = {
enableCoreExtensions: false,
};

export class BlockNoteEditor {
// TODO: make type of BareBlock / Block automatically based on options.blocks
export class BlockNoteEditor<
BareBlock extends BlockTemplate<any, any> = DefaultBlockTypes,
Block extends BareBlock & { children: Block[] } = WithChildren<BareBlock>
> {
public readonly _tiptapEditor: TiptapEditor & { contentComponent: any };
private blockCache = new WeakMap<Node, Block>();
private mousePos = { x: 0, y: 0 };
private schema = new Map<string, BlockSpec>();

public get domElement() {
return this._tiptapEditor.view.dom as HTMLDivElement;
Expand All @@ -110,19 +133,27 @@ export class BlockNoteEditor {
this._tiptapEditor.view.focus();
}

constructor(options: Partial<BlockNoteEditorOptions> = {}) {
constructor(options: Partial<BlockNoteEditorOptions<Block>> = {}) {
console.log("test");
// apply defaults
options = {
defaultStyles: true,
blocks: defaultBlocks,
...options,
};

const blockNoteExtensions = getBlockNoteExtensions({
editor: this,
uiFactories: options.uiFactories || {},
slashCommands: options.slashCommands || defaultSlashMenuItems,
blocks: options.blocks || [],
});

// add blocks to schema
for (const block of options.blocks || []) {
this.schema.set(block.type, block);
}

let extensions = options.disableHistoryExtension
? blockNoteExtensions.filter((e) => e.name !== "history")
: blockNoteExtensions;
Expand Down Expand Up @@ -192,7 +223,7 @@ export class BlockNoteEditor {
const blocks: Block[] = [];

this._tiptapEditor.state.doc.firstChild!.descendants((node) => {
blocks.push(nodeToBlock(node, this.blockCache));
blocks.push(nodeToBlock(node, this.schema, this.blockCache));

return false;
});
Expand Down Expand Up @@ -221,7 +252,7 @@ export class BlockNoteEditor {
return true;
}

newBlock = nodeToBlock(node, this.blockCache);
newBlock = nodeToBlock(node, this.schema, this.blockCache);

return false;
});
Expand Down Expand Up @@ -294,7 +325,7 @@ export class BlockNoteEditor {
return;
}

return { block: nodeToBlock(blockInfo.node, this.blockCache) };
return { block: nodeToBlock(blockInfo.node, this.schema, this.blockCache) };
}

/**
Expand Down Expand Up @@ -329,15 +360,15 @@ export class BlockNoteEditor {
}

return {
block: nodeToBlock(node, this.blockCache),
block: nodeToBlock(node, this.schema, this.blockCache),
prevBlock:
prevNode === undefined
? undefined
: nodeToBlock(prevNode, this.blockCache),
: nodeToBlock(prevNode, this.schema, this.blockCache),
nextBlock:
nextNode === undefined
? undefined
: nodeToBlock(nextNode, this.blockCache),
: nodeToBlock(nextNode, this.schema, this.blockCache),
};
}

Expand Down Expand Up @@ -389,6 +420,7 @@ export class BlockNoteEditor {
blocks.push(
nodeToBlock(
this._tiptapEditor.state.doc.resolve(pos).node(),
this.schema,
this.blockCache
)
);
Expand All @@ -408,7 +440,7 @@ export class BlockNoteEditor {
* `referenceBlock`. Inserts the blocks at the start of the existing block's children if "nested" is used.
*/
public insertBlocks(
blocksToInsert: PartialBlock[],
blocksToInsert: PartialBlockTemplate<Block>[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" | "nested" = "before"
): void {
Expand All @@ -422,7 +454,10 @@ export class BlockNoteEditor {
* @param blockToUpdate The block that should be updated.
* @param update A partial block which defines how the existing block should be changed.
*/
public updateBlock(blockToUpdate: BlockIdentifier, update: PartialBlock) {
public updateBlock(
blockToUpdate: BlockIdentifier,
update: PartialBlockTemplate<Block>
) {
updateBlock(blockToUpdate, update, this._tiptapEditor);
}

Expand All @@ -443,7 +478,7 @@ export class BlockNoteEditor {
*/
public replaceBlocks(
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock[]
blocksToInsert: PartialBlockTemplate<Block>[]
) {
replaceBlocks(blocksToRemove, blocksToInsert, this._tiptapEditor);
}
Expand Down Expand Up @@ -630,7 +665,7 @@ export class BlockNoteEditor {
* @returns The blocks parsed from the HTML string.
*/
public async HTMLToBlocks(html: string): Promise<Block[]> {
return HTMLToBlocks(html, this._tiptapEditor.schema);
return HTMLToBlocks(html, this.schema, this._tiptapEditor.schema) as any; // TODO: fix type
}

/**
Expand All @@ -651,6 +686,35 @@ export class BlockNoteEditor {
* @returns The blocks parsed from the Markdown string.
*/
public async markdownToBlocks(markdown: string): Promise<Block[]> {
return markdownToBlocks(markdown, this._tiptapEditor.schema);
return markdownToBlocks(
markdown,
this.schema,
this._tiptapEditor.schema
) as any; // TODO: fix type
}
}

// Playground:
/*
let x = new BlockNoteEditor(); // default block types are supported

// this breaks because "level" is not valid on paragraph
x.updateBlock("", { type: "paragraph", content: "hello", props: { level: "3" } });
x.updateBlock("", {
type: "heading",
content: "hello",
props: { level: "1" },
});


let y = new BlockNoteEditor<ParagraphBlockType>();

y.updateBlock("", { type: "paragraph", content: "hello", props: { } });

// this breaks because "heading" is not a type on this editor
y.updateBlock("", {
type: "heading",
content: "hello",
props: { level: "1" },
});
*/
3 changes: 3 additions & 0 deletions packages/core/src/BlockNoteExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Underline } from "@tiptap/extension-underline";
import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension";
import { BackgroundColorMark } from "./extensions/BackgroundColor/BackgroundColorMark";
import { blocks } from "./extensions/Blocks";
import { BlockSpec } from "./extensions/Blocks/api/blockTypes";
import blockStyles from "./extensions/Blocks/nodes/Block.module.css";
import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes";
import { DraggableBlocksExtension } from "./extensions/DraggableBlocks/DraggableBlocksExtension";
Expand Down Expand Up @@ -46,6 +47,7 @@ export const getBlockNoteExtensions = (opts: {
editor: BlockNoteEditor;
uiFactories: UiFactories;
slashCommands: BaseSlashMenuItem[];
blocks: BlockSpec[];
}) => {
const ret: Extensions = [
extensions.ClipboardTextSerializer,
Expand Down Expand Up @@ -88,6 +90,7 @@ export const getBlockNoteExtensions = (opts: {

// custom blocks:
...blocks,
...opts.blocks.map((b) => b.node),

Dropcursor.configure({ width: 5, color: "#ddeeff" }),
History,
Expand Down
15 changes: 8 additions & 7 deletions packages/core/src/api/blockManipulation/blockManipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Editor } from "@tiptap/core";
import { Node } from "prosemirror-model";
import {
BlockIdentifier,
PartialBlock,
BlockTemplate,
PartialBlockTemplate,
} from "../../extensions/Blocks/api/blockTypes";
import { blockToNode } from "../nodeConversions/nodeConversions";
import { getNodeById } from "../util/nodeUtil";

export function insertBlocks(
blocksToInsert: PartialBlock[],
export function insertBlocks<Block extends BlockTemplate<any, any>>(
blocksToInsert: PartialBlockTemplate<Block>[],
referenceBlock: BlockIdentifier,
placement: "before" | "after" | "nested" = "before",
editor: Editor
Expand Down Expand Up @@ -56,9 +57,9 @@ export function insertBlocks(
editor.view.dispatch(editor.state.tr.insert(insertionPos, nodesToInsert));
}

export function updateBlock(
export function updateBlock<Block extends BlockTemplate<any, any>>(
blockToUpdate: BlockIdentifier,
update: PartialBlock,
update: PartialBlockTemplate<Block>,
editor: Editor
) {
const id =
Expand Down Expand Up @@ -115,9 +116,9 @@ export function removeBlocks(
}
}

export function replaceBlocks(
export function replaceBlocks<Block extends BlockTemplate<any, any>>(
blocksToRemove: BlockIdentifier[],
blocksToInsert: PartialBlock[],
blocksToInsert: PartialBlockTemplate<Block>[],
editor: Editor
) {
insertBlocks(blocksToInsert, blocksToRemove[0], "before", editor);
Expand Down
28 changes: 17 additions & 11 deletions packages/core/src/api/formatConversions/formatConversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import remarkStringify from "remark-stringify";
import { unified } from "unified";
import { Block } from "../../extensions/Blocks/api/blockTypes";
import {
BlockSpec,
BlockTemplate,
} from "../../extensions/Blocks/api/blockTypes";

import { blockToNode, nodeToBlock } from "../nodeConversions/nodeConversions";
import { removeUnderlines } from "./removeUnderlinesRehypePlugin";
import { simplifyBlocks } from "./simplifyBlocksRehypePlugin";

export async function blocksToHTML(
blocks: Block[],
blocks: BlockTemplate<any, any>[],
schema: Schema
): Promise<string> {
const htmlParentElement = document.createElement("div");
Expand All @@ -39,25 +43,26 @@ export async function blocksToHTML(

export async function HTMLToBlocks(
html: string,
schema: Schema
): Promise<Block[]> {
schema: Map<string, BlockSpec>,
pmSchema: Schema
): Promise<BlockTemplate<any, any>[]> {
const htmlNode = document.createElement("div");
htmlNode.innerHTML = html.trim();

const parser = DOMParser.fromSchema(schema);
const parser = DOMParser.fromSchema(pmSchema);
const parentNode = parser.parse(htmlNode);

const blocks: Block[] = [];
const blocks: BlockTemplate<any, any>[] = [];

for (let i = 0; i < parentNode.firstChild!.childCount; i++) {
blocks.push(nodeToBlock(parentNode.firstChild!.child(i)));
blocks.push(nodeToBlock(parentNode.firstChild!.child(i), schema));
}

return blocks;
}

export async function blocksToMarkdown(
blocks: Block[],
blocks: BlockTemplate<any, any>[],
schema: Schema
): Promise<string> {
const markdownString = await unified()
Expand All @@ -73,14 +78,15 @@ export async function blocksToMarkdown(

export async function markdownToBlocks(
markdown: string,
schema: Schema
): Promise<Block[]> {
schema: Map<string, BlockSpec>,
pmSchema: Schema
): Promise<BlockTemplate<any, any>[]> {
const htmlString = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeStringify)
.process(markdown);

return HTMLToBlocks(htmlString.value as string, schema);
return HTMLToBlocks(htmlString.value as string, schema, pmSchema);
}
Loading