いしぐめも

プログラミングとかしたことを書きます。

Reactでも使用できるWYSIWYGエディタ「Editor.js」を使ってみた

はじめに

みなさん、ブラウザで使えるWYSIWYGエディタ Editor.js をご存知でしょうか?

editorjs.io

私は国産のパーソナルデータストアOSS「Personium」の開発コミュニティに参加しているんですが、簡単な記事投稿アプリを作ってみたくなり、今回EditorJSについて調べてみましたので、ここに書いていきたいと思います。

WYSIWYGとは

一言でいえば「編集時に出力される見た目を直接いじるような編集感を得られるエディタ」でしょうか。イメージとしては Microsoft Word に近い操作感と考えてもらえればいいと思います。

ちょっとHTMLに詳しい人がいれば「contenteditableとの違いはなに?」となるかもしれませんが、contenteditableはブラウザの実装に依る部分が大きく、抽象化して扱いやすくしてくれているOSSはとても尊いものなのです(下記noteのエンジニアのnoteがとても参考になります)。

note.com

有名なOSSでは Draft.jsEditor.js といったものがありますが、今回は後者 「Editor.js」を取り扱っていきたいと思います。

Editor.js でできること

Editor.js は読んで字のごとく「エディタ」です。エディタなので文書をエディットすることができるんですが、操作感はすごく note.com のエディタに近いです。

公式サイトに書いてある特徴としては

  • ブロックスタイルエディタであること
  • JSON形式でデータを出力すること
  • 拡張可能なシンプルなAPIが提供されていること

とあり、それぞれ簡単に説明していくと…

ブロックスタイルエディタ

ブロックスタイルとは、つまり「ここは見出し」「ここは本文」「ここは図」といった形でブロックを挿入し、文書を形成していく方式です。

f:id:yoh1496:20210624140441p:plain
ブロック挿入

f:id:yoh1496:20210624140612p:plain
見出しブロックと本文ブロック

このブロックでは何をどう表示する、といったことを決め文章を書いていきます。

f:id:yoh1496:20210624140723p:plain
プレビューを見ながら編集できるのがWYSIWYGの魅力

また、当然WYSIWYGがウリなので、それぞれのブロックがどのようなスタイルで将来レンダリングされるかを見ながら編集することができます。

JSON形式でデータを出力すること

Editor.js で出力されるデータ形式はだいたいこんな感じです。

{
  "time": "<timestamp>",
  "blocks": [ "<block>" ],
  "version": "<version>"
}

このように出力されたJSONを編集時と同じ方法でレンダリングすることでWYSIWYGが実現できるのです。

JSONでなくHTMLで出してくれればいいのに」って思うこともあるかもしれませんが、WebアプリでHTMLを直接表示できる画面を作るのは誰しも避けて通りたい道かと思います。

拡張可能なシンプルなAPI

github.com

Editor.js はブロックのレンダリングや、ブロック内のインライン要素のレンダリングプラグインを導入できる拡張性を持っています。上記リポジトリにはそういったサードパーティOSSがまとまっていて、例えばコードブロックを挿入できるプラグインや、Youtube動画などをブロックに埋め込めるプラグイン蛍光ペン的なマーカーを実現できるプラグインなどがあります。

Editor.jsのデメリット

そんなエディタがOSSで自分のサービスに導入できちゃう!すごい!!となりがちですが、Editor.js を SPA に組み込もうと思ったときに、ライフサイクルがReactのそれと異なっているのが課題となることがあります。

もしかすると他のOSSの方がレンダリングのパフォーマンスが高かったり、編集をハンドリングしやすかったりするかもしれません。それに対して自分は解を持ち合わせていません。

導入する

Reactのコンポーネントとはライフサイクルが異なるので、扱いづらい とはいえ、世の中にはReactで使用できるライブラリを提供してくれている人がいますので、今回はそれをお借りします。

github.com

使い方は簡単で、

import EditorJS from 'react-editor-js';

としてインポートしたうえで

<EditorJS data={data} onChange={handleChange} >

といった感じで使っていきます。

カスタムフックの作成

onChange でステート更新を入れると再描画されてしまうので、useRef で refを使うとか取り回しに考慮が必要になるため、ここらへんの検討事項はカスタムフックに固めてしまって使いまわせるようにしました。

import EditorJS from 'react-editor-js';

function useEditor(initialData, tools, editorId) {
  const editorInstanceRef = useRef(null);

  const getData = useCallback(async () => {
    return await editorInstanceRef.current.save();
  }, []);
  const handleRef = useCallback((ref) => {
    editorInstanceRef.current = ref;
  }, []);

  const renderEditor = () => (
    <EditorJS
      instanceRef={handleRef}
      enableReInitialize
      data={initialData}
      holder={editorId}
      tools={tools}
    >
      <div id={editorId} />
    </EditorJS>
  );

  return { getData, renderEditor };
}

initialData には編集元のデータを入れ、tools にはプラグインを指定します(後述)。editorId は複数Editorが動くようにIDを指定できるようにしました(動作未確認)

そしてデータを取りに行くときは、getData 関数を経由して、EditorJS内部のインスタンスsave() した結果を取得するようにしています。

プラグイン用のカスタムフックの作成

とりあえずMemo化して返すカスタムフックを作りましたが、、、意味あるのかコレ?

import Header from '@editorjs/header';
import Marker from '@editorjs/marker';

function useTools() {
  const tools = useMemo(
    () => ({
      header: Header,
      marker: Marker,
    }),
    []
  );
  return { tools };
}

カスタムフックを使用する

上記で作成したカスタムフックを使用する部分です。メインのコンポーネントになります

export function EditorContainer() {
  const { tools } = useTools();
  const [data, setData] = useState({
    time: 0,
    blocks: [
      {
        type: 'header',
        data: {
          text: 'initial header',
          level: 3,
        },
      },
    ],
    version: '',
  });

  const { getData, renderEditor } = useEditor(data, tools, 'editor-main');

  const handleClick = useCallback(() => {
    getData().then((dat) => setData(dat));
  }, [setData, getData]);
  return (
    <>
      <>{renderEditor()}</>
      <button onClick={handleClick}>push</button>
      <div>{JSON.stringify(data || {})}</div>
    </>
  );
}

ステート data に初期値を入れ、カスタムフック useEditor に渡します。渡した結果、renderEditor という関数が返ってくるので、それで EditorJS 用のDOMを挿入します。(styleとかどうしよう)

そしてボタンを押したら、ステート data に編集中のデータが格納され、表示されるようになっています。

結果

こんな感じにWYSIWYGなエディタを表示することができました。

f:id:yoh1496:20210624175328p:plain
編集画面

マーカープラグインをしれっと導入してみましたが、ちゃんと使えています。

ReadOnlyモード

WYSIWYGでは、表示する際のスタイルと編集する際のスタイルが一致していないと当然見た目が変わってしまいます。そこで、EditorJSでは v2.19.0 より、「Read-onlyモード」が追加されています。

github.com

これはどういうものかというと、編集に使用したEditorJSを表示にも使用できるということで、表示用と編集用を別々に開発する必要がなくなる、ということです。これは画期的ですね。(当然、プラグインもそれに対応している必要があるんですが・・・)

終わりに

以上、EditorJSでした。「WYSIWYGを扱いたいけど、ナイーブにHTMLを扱うことはしたくないなぁ」という人や、「なんでもいいから簡単に拡張できるエディタを導入したい」という人がいたらぜひ試してみてはいかがでしょうか?

次回は、IndexedDBとEditorJSを組み合わせて、下書き機能のある記事投稿アプリについて記事にしてみたいと思います。

Personiumコミュニティについて

国産パーソナルデータストアOSS「Personium」のコミュニティでは、こういったWebアプリの開発なども行っています。パーソナルデータストア(PDS)を使うことで、どういうことが実現できるのかをイメージしやすくするアプリを作っていきたいと考えています。興味のある方がいらっしゃいましたら、ぜひお声がけください。