UNPKG

@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
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 }