Torihaji's Growth Diary

Little by little, no hurry.

Nextjs で Editorjsを使ってみた part2

はじめに

ということで、part2です。

今日はエディタ複雑化週間です。

今は簡単なテキスト入力しかできないので、

見出し入力できるようにしたり、画像貼り付けられるようになったり

そういうところまでをゴールにしたいと思います。

github.com

続き

現状はこんな感じ。

なんかTextだけでとてつもなく物寂しい。

ということでなんか色々いじりたい。

調べてみるとnew するときにtoolsという項目に色々書けばいいらしい。

そして書くときにそれぞれのプラグイン?とかいうやつをinstallして。

例えば見出しなら Headerを追加インストールする必要があるとか。

ということで面白そうだから全部入れてみた。

インストールできるものとしてはここに書いてある子たち。

https://github.com/editor-js/awesome-editorjs?tab=readme-ov-file#tools

インストール方法は npm i 対象 みたいな形で それぞれのReadmeに載っていた。

paragraph

npm i @editorjs/paragraph --save

これはデフォルトで入っているらしい。これを入れることで

普通の文章入力ができるようになるのかな

今回は入れてない。カスタマイズできるそうだが、

泥沼に入りそうなのでそれはまた次の機会に

npm i @editorjs/header --save

見出しらしい。確かに追加された。

ちなみに毎回全部載せるとえげつないので

次から抜粋で。元々のコードは前回の終了時点から。

import Header from "@editorjs/header";

const Edit = () => {
  useEffect(() => {
    import("@editorjs/editorjs").then((EditorJS) => {
      const editor = new EditorJS.default({
        tools: {
          header: Header,
        },
      });
    });
  }, []);

でも選択の欄としては増えたが、大きくなっている気配がない

設定が足りないのだろうか。ということで色々調べて次のように追記。

ただshortcutとconfig配下は全部任意入力だった。

header: {
  class: Header,
  shortcut: "CMD+SHIFT+H",
  config: {
    placeholder: "Enter a header",
    levels: [2, 3, 4],
    defaultLevel: 3,
  },
},

見た目的には色々追加されてる。

ただ、大文字みたいになっていない。

なんで。

型定義のエラーも出てた

typescriptで書いているからclass: Headerのところで型定義エラーが出ていた。

型 'typeof Header' を型 'ToolConstructable | undefined' に割り当てることはできません。 型 'typeof Header' を型 'BlockToolConstructable' に割り当てることはできません。 コンストラクシグネチャの戻り値の型 'Header' と 'BlockTool' には互換性がありません。 'renderSettings()' によって返された型は、これらの型同士で互換性がありません。 型 'BlockTune' を型 'HTMLElement | MenuConfig' に割り当てることはできません。 型 'BlockTune' を型 'MenuConfigItem[]' に割り当てることはできません。 型 'BlockTune' を型 'MenuConfigItem' に割り当てることはできません。ts(2322)

確かにこの型に関しては公式にも出てた。

If you use TypeScript you need to explicitly specify that typeof Tool implements BlockToolConstructable or InlineToolConstructable

HeaderはBlockToolConstructableだから

class: Header as BlockToolConstructableとしたんだけど

型 'typeof Header' から型 'BlockToolConstructable' への変換は、互いに十分に重複できないため間違っている可能性があります。意図的にそうする場合は、まず式を 'unknown' に変換してください。 コンストラクシグネチャの戻り値の型 'Header' と 'BlockTool' には互換性がありません。 'renderSettings()' によって返された型は、これらの型同士で互換性がありません。 型 'BlockTune' は型 'HTMLElement | MenuConfig' と比較できません。 型 'BlockTune' は型 'MenuConfigItem[]' と比較できません。 型 'BlockTune' は型 'MenuConfigItem' と比較できません。

と言われた。

ということで

class: Header as unknown as BlockToolConstructableとした

そしたらいけた。

型定義も直したけど結果変わらず。

んー。アレかな。

Editorjsのimportをクライアントサイドじゃないと云々とかいうのが前回あったから

Headerのimportも非同期でできるようにしていかないとダメなのか?

ということで変更。

import { BlockToolConstructable } from "@editorjs/editorjs";
import { useEffect } from "react";

const Edit = () => {
  useEffect(() => {
    const initializeEditor = async () => {
      const EditorJS = (await import("@editorjs/editorjs")).default;
      const Header = (await import("@editorjs/header")).default;

      const editor = new EditorJS({
        tools: {
          header: {
            class: Header as unknown as BlockToolConstructable,
            shortcut: "CMD+SHIFT+H",
            config: {
              placeholder: "Enter a header",
              levels: [2, 3, 4],
              defaultLevel: 3,
            },
          },
        },
      });
    };

    initializeEditor();
  }, []);

  return <div id="editorjs"></div>;
};

export default Edit;

んー変わんない。何がいけないんだ。

levelsに1,2,3,4,5,6も入れたし、defaultLevelにも1を入れたけど変わらず。

調べて出てきたみたいにref使う方法やってみるか。

initializeEditorの最後でrefに入れてみた。

const Edit = () => {
    const editorRef = useRef<any>(null)

    const initializeEditor = async () => {

・・・・
        editorRef.current = editor
    }
}

あれか、cssか。関係なさそう。

いや関係ありそう。tailwind使ってたけど、

@tailwind base;
@tailwind components;
@tailwind utilities;

こいつら全部コメントアウトしたらh1みたくなった。

よくみたら実務の方もcssファイルをいじってた。

ということでtailwindの設定書いてたcssファイルに追記。

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Editorjs */
h1.ce-header {
  font-size: 2em;
  font-weight: bold;
}

h2.ce-header {
  font-size: 1.5em;
  font-weight: bold;
}

h3.ce-header {
  font-size: 1.17em;
  font-weight: bold;
}

h4.ce-header {
  font-size: 1em;
  font-weight: bold;
}

h5.ce-header {
  font-size: 0.83em;
  font-weight: bold;
}

h6.ce-header {
  font-size: 0.67em;
  font-weight: bold;
}

ということでCSSが大文字にならない原因だったのでheaderのimportを上に戻してみる

import { BlockToolConstructable } from "@editorjs/editorjs";
import Header from "@editorjs/header";
import { useEffect } from "react";

const Edit = () => {
  useEffect(() => {
    const initializeEditor = async () => {
      const EditorJS = (await import("@editorjs/editorjs")).default;

      const editor = new EditorJS({
        holder: "editorjs",
        tools: {
          header: {
            class: Header as unknown as BlockToolConstructable,
            shortcut: "CMD+SHIFT+H",
            inlineToolbar: true,
            config: {
              placeholder: "Enter a header",
              levels: [1, 2, 3, 4, 5, 6],
              defaultLevel: 1,
            },
          },
        },
      });
    };

    initializeEditor();
  }, []);

  return <div id="editorjs"></div>;
};

export default Edit;

ということで正常に動いた。

ということでめでたしめでたしになったのかな。

まだか。せっかくheadingのレベルを定義したのに

これってUIの方からレベル選択できるようにならないのかね。

調査開始。てかこれまだ1個目だよね。つら。

理想はこんな公式に出てるやつ

Claudeが教えてくれたけど。

npm i @codexteam/iconsして、

~~~
import {
  IconH1,
  IconH2,
  IconH3,
  IconH4,
  IconH5,
  IconH6,
  IconHeading,
} from "@codexteam/icons";
~~~

header: {
  class: Header as unknown as BlockToolConstructable,
  shortcut: "CMD+SHIFT+H",
  inlineToolbar: true,
  config: {
    placeholder: "Enter a header",
    levels: [1, 2, 3, 4, 5, 6],
    defaultLevel: 1,
  },
  toolbox: [
    {
      icon: IconH1, // H1のアイコン
      title: "Heading 1",
      data: { level: 1 },
    },
    {
      icon: IconH2, // H2のアイコン
      title: "Heading 2",
      data: { level: 2 },
    },
    {
      icon: IconH3, // H3のアイコン
      title: "Heading 3",
      data: { level: 3 },
    },
    {
      icon: IconH4, // H4のアイコン
      title: "Heading 4",
      data: { level: 4 },
    },
    {
      icon: IconH5, // H5のアイコン
      title: "Heading 5",
      data: { level: 5 },
    },
    {
      icon: IconH6, // H6のアイコン
      title: "Heading 6",
      data: { level: 6 },
    },

こうするとこんな感じ。

とりあえず元々あるHeaderにあったtoolboxをオーバライドしたらしい。

終わりに

Heading以外もやる予定でしたけど、giveup。

何これ。

はてしない。

なんでtoolboxいじったら表示されるの。

何も設定していない時にUIにHeading表示されるのは元々Headerに書かれていた

toolboxがそのHeadingを表示するように返していたからというのは理解したけど。

これね。Githubに書いてあった。

https://github.com/editor-js/header/blob/master/src/index.ts#L26

  /**
   * Get Tool toolbox settings
   * icon - Tool icon's SVG
   * title - title to show in toolbox
   *
   * @returns {{icon: string, title: string}}
   */
  static get toolbox() {
    return {
      icon: IconHeading,
      title: 'Heading',
    };
  }

謎は深まるばかり。

追記 配列で渡したらなんで上書きされるか。

githubに書いてあった。多分これ。

toolboxにオブジェクト渡すか配列渡すかで挙動が違うらしい。

オブジェクト渡したらデフォルトのやつとマージされ、

User定義の方で配列でtoolbox渡したらUserの方が優先されて

表示されるっぽい。だから配列で渡したらUser定義が優先されたのか。

ちなみにデフォルトのtoolboxは上に書いたようにオブジェクトで渡ってる。

それに対して今回渡したのは配列。だから今回の結果になった。納得。

これで寝れる。

https://github.com/codex-team/editor.js/blob/next/src/components/tools/block.ts#L87

public get toolbox(): ToolboxConfigEntry[] | undefined {
    const toolToolboxSettings = this.constructable[InternalBlockToolSettings.Toolbox] as ToolboxConfig;
    const userToolboxSettings = this.config[UserSettings.Toolbox];

    if (_.isEmpty(toolToolboxSettings)) {
      return;
    }
    if (userToolboxSettings === false) {
      return;
    }
    /**
     * Return tool's toolbox settings if user settings are not defined
     */
    if (!userToolboxSettings) {
      return Array.isArray(toolToolboxSettings) ? toolToolboxSettings : [ toolToolboxSettings ];
    }

    /**
     * Otherwise merge user settings with tool's settings
     */
    if (Array.isArray(toolToolboxSettings)) {
      if (Array.isArray(userToolboxSettings)) {
        return userToolboxSettings.map((item, i) => {
          const toolToolboxEntry = toolToolboxSettings[i];

          if (toolToolboxEntry) {
            return {
              ...toolToolboxEntry,
              ...item,
            };
          }

          return item;
        });
      }

      return [ userToolboxSettings ];
    } else {
      if (Array.isArray(userToolboxSettings)) {
        return userToolboxSettings; <= ここね。
      }

      return [
        {
          ...toolToolboxSettings,
          ...userToolboxSettings,
        },
      ];
    }
  }