Skip to content

editor.blocksToHTML does not include custom blocks #234

New issue

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

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

Already on GitHub? Sign in 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

2 participants