Skip to content

editor.blocksToHTML does not include custom blocks #234

New issue

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

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

Already on GitHub? # to your account

Closed
maxswjeon opened this issue Jun 6, 2023 · 1 comment
Closed

editor.blocksToHTML does not include custom blocks #234

maxswjeon opened this issue Jun 6, 2023 · 1 comment
Labels
duplicate This issue or pull request already exists

Comments

@maxswjeon
Copy link

maxswjeon commented Jun 6, 2023

SSIA, editor.blocksToHTML does not include custom blocks.

The editor HTML

<div class="_blockGroup_1a5p7_42" data-node-type="blockGroup">
    <div data-id="ff23a58f-114e-4b6e-8fe5-fd8dde0acf82" data-text-color="black" data-background-color="transparent" class="_blockOuter_1a5p7_5" data-node-type="block-outer">
        <div data-id="ff23a58f-114e-4b6e-8fe5-fd8dde0acf82" data-text-color="black" data-background-color="transparent" class="_block_1a5p7_5" data-node-type="blockContainer">
            <div class="react-renderer node-image _reactNodeViewRenderer_1a5p7_17 _isEmpty_1a5p7_240">
                <div class="_blockContent_1a5p7_22" data-content-type="image" data-background-color="transparent" data-text-color="black" data-text-alignment="left" data-src="/images/1e917b5b-f1dd-423c-9141-6f109f688559.jpeg" data-caption="" data-node-view-wrapper="" style="white-space: normal;">
                    <div class="flex flex-col" id="ff23a58f-114e-4b6e-8fe5-fd8dde0acf82">
                        <img class="w-full" src="/images/1e917b5b-f1dd-423c-9141-6f109f688559.jpeg" alt="" contenteditable="false">
                        <div class="hidden _inlineContent_1a5p7_240" data-node-view-content="" style="white-space: pre-wrap;">
                            <div style="white-space: inherit;">
                                <br class="ProseMirror-trailingBreak">
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div data-id="2800aef9-5ae5-4e4b-8743-c2bf3cd3e32d" class="_blockOuter_1a5p7_5" data-node-type="block-outer">
        <div data-id="2800aef9-5ae5-4e4b-8743-c2bf3cd3e32d" class="_block_1a5p7_5" data-node-type="blockContainer">
            <div class="_blockContent_1a5p7_22 _isEmpty_1a5p7_240" data-content-type="paragraph">
                <p class="_inlineContent_1a5p7_240"><br class="ProseMirror-trailingBreak"></p>
            </div>
        </div>
    </div>
    <div data-id="a16de5f5-30fe-40fb-98f0-796540472592" class="_blockOuter_1a5p7_5" data-node-type="block-outer">
        <div data-id="a16de5f5-30fe-40fb-98f0-796540472592" class="_block_1a5p7_5" data-node-type="blockContainer">
            <div class="_blockContent_1a5p7_22" data-content-type="paragraph">
                <p class="_inlineContent_1a5p7_240"> </p>
            </div>
        </div>
    </div>
    <div data-id="60964bbd-2dfd-48e3-98c5-1aca8719413a" class="_blockOuter_1a5p7_5" data-node-type="block-outer">
        <div data-id="60964bbd-2dfd-48e3-98c5-1aca8719413a" class="_block_1a5p7_5" data-node-type="blockContainer">
            <div class="_blockContent_1a5p7_22" data-content-type="paragraph">
                <p class="_inlineContent_1a5p7_240">Test</p>
            </div>
        </div>
    </div>
    <div data-id="b6249634-f473-46a6-b8c3-7af5f6b22782" class="_blockOuter_1a5p7_5" data-node-type="block-outer">
        <div data-id="b6249634-f473-46a6-b8c3-7af5f6b22782" class="_block_1a5p7_5" data-node-type="blockContainer">
            <div class="react-renderer node-codeblock _reactNodeViewRenderer_1a5p7_17">
                <div class="_blockContent_1a5p7_22" data-content-type="codeblock" data-language="plaintext" data-node-view-wrapper="" style="white-space: normal;">
                    <div class="relative bg-stone-100 p-6">
                        <select class="absolute top-6 right-6 text-sm w-[100px] bg-stone-100 focus:outline-none">
                            <option class="p-3" value="plaintext">Plain Text</option>
                            <option class="p-3" value="javascript">javascript</option>
                        </select>
                        <code class="language-plaintext">
                            <div class="_inlineContent_1a5p7_240" data-node-view-content="" style="white-space: pre-wrap;">
                                <div style="white-space: inherit;">```Test```</div>
                            </div>
                        </code>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div data-id="2b071916-6ec2-495b-98a7-2353e8586afe" class="_blockOuter_1a5p7_5" data-node-type="block-outer">
        <div data-id="2b071916-6ec2-495b-98a7-2353e8586afe" class="_block_1a5p7_5" data-node-type="blockContainer">
            <div class="_blockContent_1a5p7_22 _isEmpty_1a5p7_240" data-content-type="paragraph">
                <p class="_inlineContent_1a5p7_240">
                    <br class="ProseMirror-trailingBreak">
                </p>
            </div>
        </div>
    </div>
</div>

The result of editor.blocksToHTML

<div></div>
<p class="_inlineContent_1a5p7_240"></p>
<p class="_inlineContent_1a5p7_240"> </p>
<p class="_inlineContent_1a5p7_240">Test</p>
<div>```Test```</div>
<p class="_inlineContent_1a5p7_240"></p>

Reproduction

Editor.tsx

import {
  BlockNoteView,
  defaultReactSlashMenuItems,
  useBlockNote,
} from "@blocknote/react";

import { defaultBlockSchema } from "@blocknote/core";

import { CodeBlock, CodeCommand } from "./CodeBlock";
import { ImageBlock, ImageCommand } from "./ImageBlock";

import "@blocknote/core/style.css";

export default function Editor({
  content,
  setContent,
  save,
  disabled,
}: Props) {
  const contentEditor = useBlockNote({
    blockSchema: {
      ...defaultBlockSchema,
      image: ImageBlock,
      codeblock: CodeBlock,
    },
    slashCommands: [
      ...defaultReactSlashMenuItems,
      ImageCommand,
      CodeCommand,
    ],
    initialContent: JSON.parse(content || "[]"),
    onEditorContentChange: async (editor) => {
      setContent(JSON.stringify(editor.topLevelBlocks));
    },
    editable: !disabled,
  });

  return (
    <div className="flex-1 overflow-y-auto">
      <BlockNoteView editor={contentEditor} />
    </div>
  );
}

CodeBlock.tsx

import { DefaultBlockSchema } from "@blocknote/core";
import {
  InlineContent,
  ReactSlashMenuItem,
  createReactBlockSpec,
} from "@blocknote/react";

import IconCode from "assets/icons/icon_code.svg";

export const CodeBlock = createReactBlockSpec({
  type: "codeblock",
  propSchema: {
    language: {
      default: "plaintext",
    },
  },
  containsInlineContent: true,
  render: ({ block, editor }) => {
    return (
      <div className="relative bg-stone-100 p-6">
        <select
          className="absolute top-6 right-6 text-sm w-[100px] bg-stone-100 focus:outline-none"
          onChange={(e) => {
            editor.updateBlock(block, {
              props: {
                ...block.props,
                language: e.target.value,
              },
            });
          }}
        >
          <option className="p-3" value="plaintext">
            Plain Text
          </option>
          <option className="p-3" value="javascript">
            javascript
          </option>
        </select>
        <code className={`language-${block.props.language}`}>
          <InlineContent />
        </code>
      </div>
    );
  },
});

export const CodeCommand = new ReactSlashMenuItem<
  DefaultBlockSchema & { codeblock: typeof CodeBlock }
>(
  "Code",
  (editor) => {
    if (editor.getTextCursorPosition().block.content.length === 0) {
      editor.updateBlock(editor.getTextCursorPosition().block, {
        type: "codeblock",
        props: {},
      });
      return;
    }

    editor.insertBlocks(
      [
        {
          type: "codeblock",
          props: {},
        },
      ],
      editor.getTextCursorPosition().block,
      "after"
    );
  },
  ["code"],
  "Text",
  <IconCode className="w-5 h-5" />,
  "Insert a Code Block"
);

ImageBlock.tsx

import { useRef } from "react";

import { DefaultBlockSchema, defaultProps } from "@blocknote/core";
import {
  InlineContent,
  ReactSlashMenuItem,
  createReactBlockSpec,
} from "@blocknote/react";

import axios from "axios";
import mime from "mime-types";

import IconPicture from "assets/icons/icon_image.svg";

type UploadResponse = {
  result: true;
  data: {
    filename: string;
  };
};

export const ImageBlock = createReactBlockSpec({
  type: "image",
  propSchema: {
    ...defaultProps,
    src: {
      default: "",
    },
    caption: {
      default: "",
    },
  },
  containsInlineContent: true, // For the caption
  render: ({ block, editor }) => {
    const inputRef = useRef<HTMLInputElement>(null);

    const onSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
      const file = e.target.files?.[0];
      if (!file) return;

      const { data } = await axios.post<UploadResponse>("/api/image", file, {
        withCredentials: true,
        headers: {
          "Content-Type": mime.contentType(file.name),
        },
      });

      editor.updateBlock(block, {
        props: {
          ...block.props,
          src: `/images/${data.data.filename}`,
        },
      });
    };

    return (
      <div className="flex flex-col" id={block.id}>
        {block.props.src && (
          <>
            <img
              className="w-full"
              src={block.props.src}
              alt=""
              contentEditable={false}
            />
          </>
        )}
        {!block.props.src && (
          <div
            className="w-full bg-stone-100 flex items-center p-6"
            contentEditable={false}
            onClick={() => inputRef.current?.click()}
            onKeyDown={() => inputRef.current?.click()}
          >
            <input
              type="file"
              className="hidden"
              ref={inputRef}
              onChange={onSelect}
            />
            <IconPicture
              className="w-5 h-5 mr-3 fill-gray-600"
              contentEditable={false}
            />
            <p className="text-xl text-gray-600" contentEditable={false}>
              Add an image
            </p>
          </div>
        )}
        <InlineContent className={block.props.caption ? "block" : "hidden"} />
      </div>
    );
  },
});

export const ImageCommand = new ReactSlashMenuItem<
  DefaultBlockSchema & { image: typeof ImageBlock }
>(
  "Insert Image",
  (editor) => {
    if (editor.getTextCursorPosition().block.content.length === 0) {
      editor.updateBlock(editor.getTextCursorPosition().block, {
        type: "image",
        props: {},
      });
      return;
    }

    editor.insertBlocks(
      [
        {
          type: "image",
          props: {},
        },
      ],
      editor.getTextCursorPosition().block,
      "after",
    );
  },
  ["image", "img", "picture", "media"],
  "Media",
  <IconPicture className="w-5 h-5" />,
  "Insert an image",
);
@YousefED
Copy link
Collaborator

YousefED commented Jun 6, 2023

Thanks for reporting. I'm marking this as a duplicate of #221

We have a good idea for a solution, but didn't get to this yet

@YousefED YousefED closed this as completed Jun 6, 2023
@YousefED YousefED added the duplicate This issue or pull request already exists label Jun 6, 2023
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

2 participants