[React] react-draft-wysiwygを用いたリッチテキストエディタ

## 実行環境
MacOS BigSur -- 11.2.1 
npm -- 6.14.4
react -- 17.0.1
react-dom -- 17.0.1
react-hook-form -- 7.0.0
draft-js -- 0.11.7
draft-convert -- 2.1.11
react-draft-wysiwyg -- 1.14.7
dompurify -- 2.2.8

## リッチテキストエディタを実装したい
Reactアプリにリッチテキストエディタの実装をしたい、と考えたところFacebook製の[Draft.js](https://draftjs.org/)が良いという記事をいくつか見ました。
今回は、[こちら](https://qiita.com/so99ynoodles/items/5b4f237e03dadc42e751)で紹介されていたDjaft.jsのライブラリである[react-draft-wysiwyg](https://jpuri.github.io/react-draft-wysiwyg/#/)を使用して実装してみました。

## エディタ基本実装
まずはライブラリをインストールします。

```
$ npm install draft-js
$ npm install react-draft-wysiwyg
```

公式ドキュメントのチュートリアルにあるコードを元に、まずは簡単なエディタを作成します。

```editor.js
import React, { Component } from 'react';
import { Editor } from 'react-draft-wysiwyg';
//node_module内のcssファイルを読み込む
import '../../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css';

const Editor = () => {
 <Editor
  wrapperClassName="wrapper-class"
  editorClassName="editor-class"
  toolbarClassName="toolbar-class"
  wrapperStyle={<wrapperStyleObject>}
  editorStyle={<editorStyleObject>}
  toolbarStyle={<toolbarStyleObject>}
  toolbar={{
      options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'colorPicker', 'link', 'history'],
      inline: { inDropdown: true },
      list: { inDropdown: true },
      textAlign: { inDropdown: true },
      link: { inDropdown: true },
      history: { inDropdown: true },
      blockType: { options: ['Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote', 'Code'], },
   }}
  localization={{
    locale: 'ja',
  }}
 />
}
export default Editor;
```
まずはツールバーとエディタが表示されます。
![スクリーンショット 2021-05-14 23.17.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681173/564c958b-7429-a9ea-8e0a-d2c80b9d11fd.png)
ちなみに、node_module内のcssファイルを読み込む際には、自分のプロジェクト構成からパスを設定する必要があります。
```toolbar = {}```
の部分でツールバーの設定を行います。公式ドキュメントにそれぞれの概要が書いてありますので、編集しましょう。
```localization = {{locale: 'ja',}}```
の部分で日本語に設定できます。この設定をしない場合は、ツールバーの各表示がDefaultの英語表記になっています。

### CSS編集
エディタの見た目の調整は、以下を記述することで可能です。
```wrapperClassName="wrapper-class"
editorClassName="editor-class"
toolbarClassName="toolbar-class"
```

```index.css
.wrapper-class {
  padding: 1rem;
  margin-left: 1rem;
  margin-right: 1rem;
  border: 2px solid #ccc;
}
.editor-class {
  background-color:lightgray;
  padding: 1rem;
  border: 2px solid #ccc;
}
.toolbar-class {
  border: 2px solid #ccc;
}
```
このようにcssファイルで各要素のスタイルを変更することで、上記のような出力になります。

## リッチテキストの表示
通常エディタを実装する場合は入力した内容を保存・出力する必要があります。先程の表示させただけのエディタから、その入力内容を表示する実装を行います。
### エディタの状態
エディタ内部の状態管理について、editorStateとonEditorStateChangeがあります。

- editorState - エディタの状態を更新する
- onEditorStateChange - エディタの状態が変化したときに呼び出される関数

例えば、初期状態を以下のように定義してeditorStateに設定すると、エディタ表示時に初期テキストが表示されます。

```editor.js
...
 const initData = convertFromRaw({
    entityMap: {},
    blocks: [
      {
        key: "xxxxxx", // ユニークなキー値
        text: "ここに初期テキストがはいります。", // 任意のテキスト
        type: "unstyled", // テキストのタイプ。初期値は "unstyled"
        depth: 0,
        entityRanges: [],
        inlineStyleRanges: [],
        data: {},
      },
    ],
  })
 const initState = EditorState.createWithContent(initData,);
 const [editorState, setEditorState] = useState(initState);

 return(
  <div>
   <Editor
    ...
    editorState={editorState}
   />
  )}
```
![スクリーンショット 2021-05-14 23.33.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681173/3d0a8179-f559-384a-16c9-47a737a243ad.png)

### データ変換
テキストの保存、出力のために必要となるデータ変換を行うために、Draft.jsは以下の3つの関数を提供してくれているらしいです。

- ContentFromRaw - raw state(RawDraftContentState)をContentStateに変換
- ContentToRaw - 上記の逆変換
- ContentFromHTML - HTMLをContentBlockオブジェクトの配列、entityMapオブジェクトに変換

他にもエディタ内の状態をHTMLに変換する必要がありますが、これにdraft-convertを利用します。

```
$ npm install draft-convert
```
このパッケージにより、convertToHTML関数を使用してHTML変換が可能となります。

### HTMLのサニタイズ
さて、後はHTML変換したものをページ上に表示させるだけですが、HTMLをページに追加する前にHTMLが適切に構造化及びサニタイズ（危険なコードやデータを変換または除去して無力化する処理）されているかチェックする必要があります。この処理を怠ると、クロスサイトスクリプティング（XSS）の危険性が高まります。

この処理を簡単に実行してくれるのが、dompurifyパッケージです。

```
$ npm install dompurify
```
ちなみに、このpurifyという英単語に馴染みがなく、Google翻訳すると「祓い清める」と変換されてました大袈裟な気が。。笑
何はともあれ、dompurifyによりサニタイズされたHTMLを取得できます。

## 実装及び表示確認
実際の実装手順を整理します。
1. エディタに入力されている現在のコンテンツの取得
　getCurrentContentメソッド - エディタの現在のコンテンツを取得
2. HTMLへ変換
　setConvertContent
3. テキストの表示
　dangerouslySetInnerHTML - HTMLが適切に構造化及びサニタイズされているか確認

```Editor.js
import React, { useState } from 'react';
import { EditorState,convertFromRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import DOMPurify from 'dompurify';
import { convertToHTML } from 'draft-convert';
import '../../node_modules/react-draft-wysiwyg/dist/react-draft-wysiwyg.css';


const Editor = () => {
  const initData = convertFromRaw({
    entityMap: {},
    blocks: [
      {
        key: "xxxxxx", // ユニークなキー値
        text: "ここに初期テキストがはいります。", // 任意のテキスト
        type: "unstyled", // テキストのタイプ。初期値は "unstyled"
        depth: 0,
        entityRanges: [],
        inlineStyleRanges: [],
        data: {},
      },
    ],
  })
  
  const initState = EditorState.createWithContent(
    initData,
  )
  const [editorState, setEditorState] = useState(initState);
  const [convertedContent, setConvertedContent] = useState(null);

  const handleEditorChange = (state) => {
    setEditorState(state);
    convertContentToHTML();
  }
  const convertContentToHTML = () => {
    let currentContentAsHTML = convertToHTML(editorState.getCurrentContent());
    setConvertedContent(currentContentAsHTML);
  }

  const createMarkup = (html) => {
    return  {
      __html: DOMPurify.sanitize(html)
    }
  }

  return(
  <div>
  <Editor
    editorState={editorState}
    onEditorStateChange={handleEditorChange}
    wrapperClassName="wrapper-class"
    editorClassName="editor-class"
    toolbarClassName="toolbar-class"
    toolbar={{
      options: ['inline', 'blockType', 'fontSize', 'list', 'textAlign', 'colorPicker', 'link', 'history'],
      inline: { inDropdown: true },
      list: { inDropdown: true },
      textAlign: { inDropdown: true },
      link: { inDropdown: true },
      history: { inDropdown: true },
      blockType: { options: ['Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote', 'Code'], },
   }}
   localization={{
    locale: 'ja',
   }}
  />
  <div className="preview" 
  dangerouslySetInnerHTML={createMarkup(convertedContent)}></div>
  </div>
  )}

export default Editor;
```
![スクリーンショット 2021-05-14 23.54.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681173/ef50d369-f519-2215-f96f-6c46f4ed651b.png)

このように、エディタに入力した内容を正しく下の部分に表示できています！！

まだまだ勉強すべきことがたくさんありますが、とりあえず簡単なエディタ操作ができました。それにしてもreact-draft-wyswygの日本語記事が少なすぎる。。。リンクや画像、文字色などにはまだ対応できていないので学習を続けようと思います。

## 参考
以下のページが非常に分かりやすく、参考にさせていただきました。
- https://blog.logrocket.com/building-rich-text-editors-in-react-using-draft-js-and-react-draft-wysiwyg/
- https://www.to-r.net/media/draftjs-tips/
- https://qiita.com/so99ynoodles/items/5b4f237e03dadc42e751