import { addInlineContentAttributes, addInlineContentKeyboardShortcuts, BlockNoteEditor, camelToDataKebab, createInternalInlineContentSpec, CustomInlineContentConfig, CustomInlineContentImplementation, getInlineContentParseRules, InlineContentFromConfig, InlineContentSchemaWithInlineContent, InlineContentSpec, inlineContentToNodes, nodeToCustomInlineContent, PartialCustomInlineContentFromConfig, Props, PropSchema, propsToAttributes, StyleSchema, } from "@blocknote/core"; import { Node } from "@tiptap/core"; import { NodeViewProps, NodeViewWrapper, ReactNodeViewRenderer, useReactNodeView, } from "@tiptap/react"; // import { useReactNodeView } from "@tiptap/react/dist/packages/react/src/useReactNodeView"; import { FC, JSX } from "react"; import { renderToDOMSpec } from "./@util/ReactRenderUtil.js"; // this file is mostly analogoues to `customBlocks.ts`, but for React blocks export type ReactCustomInlineContentRenderProps< T extends CustomInlineContentConfig, S extends StyleSchema, > = { inlineContent: InlineContentFromConfig; updateInlineContent: ( update: PartialCustomInlineContentFromConfig, ) => void; editor: BlockNoteEditor< any, InlineContentSchemaWithInlineContent, S >; contentRef: (node: HTMLElement | null) => void; }; // extend BlockConfig but use a React render function export type ReactInlineContentImplementation< T extends CustomInlineContentConfig, // I extends InlineContentSchema, S extends StyleSchema, > = { render: FC>; toExternalHTML?: FC>; } & Omit, "render" | "toExternalHTML">; // Function that adds a wrapper with necessary classes and attributes to the // component returned from a custom inline content's 'render' function, to // ensure no data is lost on internal copy & paste. export function InlineContentWrapper< IType extends string, PSchema extends PropSchema, >(props: { children: JSX.Element; inlineContentType: IType; inlineContentProps: Props; propSchema: PSchema; }) { return ( // Creates inline content section element { const spec = props.propSchema[prop]; return value !== spec.default; }) .map(([prop, value]) => { return [camelToDataKebab(prop), value]; }), )} > {props.children} ); } /** * Creates a custom inline content specification for use with React. This is the * React counterpart to the vanilla `createInlineContentSpec` and lets you define * custom inline content types (e.g., mentions, tags) using React components for * rendering. * * @param inlineContentConfig - The inline content type configuration, including * its `type` name, `propSchema`, and `content` mode (`"styled"` or `"none"`). * @param inlineContentImplementation - The React implementation, including a * `render` component and optionally a `toExternalHTML` component and `parse` * rules. * @returns An `InlineContentSpec` that can be passed to the editor's schema. */ export function createReactInlineContentSpec< const T extends CustomInlineContentConfig, // I extends InlineContentSchema, S extends StyleSchema, >( inlineContentConfig: T, inlineContentImplementation: ReactInlineContentImplementation, ): InlineContentSpec { const node = Node.create({ name: inlineContentConfig.type as T["type"], inline: true, group: "inline", selectable: inlineContentConfig.content === "styled", atom: inlineContentConfig.content === "none", draggable: inlineContentImplementation.meta?.draggable, content: (inlineContentConfig.content === "styled" ? "inline*" : "") as T["content"] extends "styled" ? "inline*" : "", addAttributes() { return propsToAttributes(inlineContentConfig.propSchema); }, addKeyboardShortcuts() { return addInlineContentKeyboardShortcuts(inlineContentConfig); }, parseHTML() { return getInlineContentParseRules( inlineContentConfig, inlineContentImplementation.parse, ); }, renderHTML({ node }) { const editor = this.options.editor; const ic = nodeToCustomInlineContent( node, editor.schema.inlineContentSchema, editor.schema.styleSchema, ) as any as InlineContentFromConfig; // TODO: fix cast const Content = inlineContentImplementation.toExternalHTML || inlineContentImplementation.render; const output = renderToDOMSpec( (ref) => ( { ref(element); if (element) { element.dataset.editable = ""; } }} inlineContent={ic} updateInlineContent={() => { // No-op }} editor={editor} /> ), editor, ); return addInlineContentAttributes( output, inlineContentConfig.type, node.attrs as Props, inlineContentConfig.propSchema, ); }, addNodeView() { const editor: BlockNoteEditor = this.options.editor; return (props) => ReactNodeViewRenderer( (props: NodeViewProps) => { const ref = useReactNodeView().nodeViewContentRef; if (!ref) { throw new Error("nodeViewContentRef is not set"); } const Content = inlineContentImplementation.render; return ( } inlineContentType={inlineContentConfig.type} propSchema={inlineContentConfig.propSchema} > { ref(element); if (element) { element.dataset.editable = ""; } }} editor={editor} inlineContent={ nodeToCustomInlineContent( props.node, editor.schema.inlineContentSchema, editor.schema.styleSchema, ) as any as InlineContentFromConfig // TODO: fix cast } updateInlineContent={(update) => { const content = inlineContentToNodes( [update], editor.pmSchema, ); const pos = props.getPos(); if (pos === undefined) { return; } editor.transact((tr) => tr.replaceWith(pos, pos + props.node.nodeSize, content), ); }} /> ); }, { className: "bn-ic-react-node-view-renderer", as: "span", // contentDOMElementTag: "span", (requires tt upgrade) }, )(props); }, }); return createInternalInlineContentSpec( inlineContentConfig as CustomInlineContentConfig, { ...inlineContentImplementation, node, render(inlineContent, updateInlineContent, editor) { const Content = inlineContentImplementation.render; const output = renderToDOMSpec((ref) => { return ( { ref(element); if (element) { element.dataset.editable = ""; } }} editor={editor} inlineContent={inlineContent} updateInlineContent={updateInlineContent} /> ); }, editor); return output; }, toExternalHTML(inlineContent, editor) { const Content = inlineContentImplementation.toExternalHTML || inlineContentImplementation.render; const output = renderToDOMSpec((ref) => { return ( { ref(element); if (element) { element.dataset.editable = ""; } }} editor={editor} inlineContent={inlineContent} updateInlineContent={() => { // no-op }} /> ); }, editor); return output; }, }, ) as any; }