import { NodeViewContent, NodeViewProps, NodeViewWrapper, ReactNodeViewRenderer, } from "@tiptap/react"; import { BlockConfig, BlockSchema, BlockSpec, PropSchema, blockStyles, camelToDataKebab, createTipTapBlock, parse, propsToAttributes, render, } from "bocknoat-core"; import { FC, HTMLAttributes } from "react"; // extend BlockConfig but use a react render function export type ReactBlockConfig< Type extends string, PSchema extends PropSchema, ContainsInlineContent extends boolean, BSchema extends BlockSchema > = Omit< BlockConfig, "render" > & { render: FC<{ block: Parameters< BlockConfig["render"] >[0]; editor: Parameters< BlockConfig["render"] >[1]; }>; }; export const InlineContent = (props: HTMLAttributes) => ( ); // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactBlockSpec< BType extends string, PSchema extends PropSchema, ContainsInlineContent extends boolean, BSchema extends BlockSchema >( blockConfig: ReactBlockConfig ): BlockSpec { const node = createTipTapBlock({ name: blockConfig.type, content: (blockConfig.containsInlineContent ? "inline*" : "") as ContainsInlineContent extends true ? "inline*" : "", selectable: blockConfig.containsInlineContent, addOptions() { return { editor: undefined, }; }, addAttributes() { return propsToAttributes(blockConfig); }, parseHTML() { return parse(blockConfig); }, renderHTML({ HTMLAttributes }) { return render(blockConfig, HTMLAttributes); }, addNodeView() { const BlockContent: FC = (props: NodeViewProps) => { const Content = blockConfig.render; // Add custom HTML attributes const blockContentDOMAttributes = this.options.domAttributes?.blockContent || {}; console.log("blockContentDOMAttributes", blockContentDOMAttributes); // Add props as HTML attributes in kebab-case with "data-" prefix const htmlAttributes: Record = {}; for (const [attribute, value] of Object.entries(props.node.attrs)) { if ( attribute in blockConfig.propSchema && value !== blockConfig.propSchema[attribute].default ) { htmlAttributes[camelToDataKebab(attribute)] = value; } } console.log("htmlAttributes", htmlAttributes); // Gets BlockNote editor instance const editor = this.options.editor!; console.log("props", props); console.log("editor", editor); // Gets position of the node const pos = typeof props.getPos === "function" ? props.getPos() : undefined; console.log("pos", pos); // Gets TipTap editor instance const tipTapEditor = editor._tiptapEditor; // Gets parent blockContainer node const blockContainer = tipTapEditor.state.doc.resolve(pos!).node(); console.log("blockContainer", blockContainer); // Gets block identifier const blockIdentifier = blockContainer.attrs.id; console.log("blockIdentifier", blockIdentifier); // Get the block const block = editor.getBlock(blockIdentifier)!; if (block.type !== blockConfig.type) { throw new Error("Block type does not match"); } return ( key !== "class" ) )} className={blockStyles.blockContent} data-content-type={blockConfig.type} {...htmlAttributes}> ); }; return ReactNodeViewRenderer(BlockContent, { className: blockStyles.reactNodeViewRenderer, }); }, }); return { node: node, propSchema: blockConfig.propSchema, }; }