@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
163 lines (143 loc) • 4.93 kB
text/typescript
import { Node } from "@tiptap/core";
import { TagParseRule } from "@tiptap/pm/model";
import { inlineContentToNodes } from "../../api/nodeConversions/blockToNode.js";
import { nodeToCustomInlineContent } from "../../api/nodeConversions/nodeToBlock.js";
import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import { propsToAttributes } from "../blocks/internal.js";
import { Props } from "../propTypes.js";
import { StyleSchema } from "../styles/types.js";
import {
addInlineContentAttributes,
addInlineContentKeyboardShortcuts,
createInlineContentSpecFromTipTapNode,
} from "./internal.js";
import {
CustomInlineContentConfig,
InlineContentFromConfig,
InlineContentSpec,
PartialCustomInlineContentFromConfig,
} from "./types.js";
// TODO: support serialization
export type CustomInlineContentImplementation<
T extends CustomInlineContentConfig,
// B extends BlockSchema,
// I extends InlineContentSchema,
S extends StyleSchema,
> = {
render: (
/**
* The custom inline content to render
*/
inlineContent: InlineContentFromConfig<T, S>,
updateInlineContent: (
update: PartialCustomInlineContentFromConfig<T, S>,
) => void,
/**
* The BlockNote editor instance
* This is typed generically. If you want an editor with your custom schema, you need to
* cast it manually, e.g.: `const e = editor as BlockNoteEditor<typeof mySchema>;`
*/
editor: BlockNoteEditor<any, any, S>,
// (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations
// or allow manually passing <BSchema>, but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics
) => {
dom: HTMLElement;
contentDOM?: HTMLElement;
// destroy?: () => void;
};
};
export function getInlineContentParseRules(
config: CustomInlineContentConfig,
): TagParseRule[] {
return [
{
tag: `[data-inline-content-type="${config.type}"]`,
contentElement: (element) => {
const htmlElement = element as HTMLElement;
if (htmlElement.matches("[data-editable]")) {
return htmlElement;
}
return htmlElement.querySelector("[data-editable]") || htmlElement;
},
},
];
}
export function createInlineContentSpec<
T extends CustomInlineContentConfig,
S extends StyleSchema,
>(
inlineContentConfig: T,
inlineContentImplementation: CustomInlineContentImplementation<T, S>,
): InlineContentSpec<T> {
const node = Node.create({
name: inlineContentConfig.type,
inline: true,
group: "inline",
selectable: inlineContentConfig.content === "styled",
atom: inlineContentConfig.content === "none",
content: (inlineContentConfig.content === "styled"
? "inline*"
: "") as T["content"] extends "styled" ? "inline*" : "",
addAttributes() {
return propsToAttributes(inlineContentConfig.propSchema);
},
addKeyboardShortcuts() {
return addInlineContentKeyboardShortcuts(inlineContentConfig);
},
parseHTML() {
return getInlineContentParseRules(inlineContentConfig);
},
renderHTML({ node }) {
const editor = this.options.editor;
const output = inlineContentImplementation.render(
nodeToCustomInlineContent(
node,
editor.schema.inlineContentSchema,
editor.schema.styleSchema,
) as any as InlineContentFromConfig<T, S>, // TODO: fix cast
() => {
// No-op
},
editor,
);
return addInlineContentAttributes(
output,
inlineContentConfig.type,
node.attrs as Props<T["propSchema"]>,
inlineContentConfig.propSchema,
);
},
addNodeView() {
return ({ node, getPos }) => {
const editor = this.options.editor as BlockNoteEditor<any, any, S>;
const output = inlineContentImplementation.render(
nodeToCustomInlineContent(
node,
editor.schema.inlineContentSchema,
editor.schema.styleSchema,
) as any as InlineContentFromConfig<T, S>, // TODO: fix cast
(update) => {
if (typeof getPos === "boolean") {
return;
}
const content = inlineContentToNodes([update], editor.pmSchema);
editor.transact((tr) =>
tr.replaceWith(getPos(), getPos() + node.nodeSize, content),
);
},
editor,
);
return addInlineContentAttributes(
output,
inlineContentConfig.type,
node.attrs as Props<T["propSchema"]>,
inlineContentConfig.propSchema,
);
};
},
});
return createInlineContentSpecFromTipTapNode(
node,
inlineContentConfig.propSchema,
) as InlineContentSpec<T>; // TODO: fix cast
}