Editor.jsを使ってテキストエディタを作る


## はじめに
Editor.jsを使ってテキストエディタを実装する方法を解説します。
本記事ではNuxt.jsを使ってますが、Vue.jsやJavaScript単体でも使用できます。
主に[公式のガイド](https://editorjs.io/base-concepts)を参考にして、できるだけ丁寧に解説するので、ぜひお付き合いください。

## Editor.jsの概要
[Editor.js](https://editorjs.io)とはテキストエディタの開発するためのJavaScriptライブラリです。

Editor.jsの特徴として

- 流行りのブロックスタイル
- 出力がJSON形式で汎用的
- カスタマイズ性が高い

があげられ、シンプルかつ自分好みのエディタを簡単に実装できます。


## 導入
まずライブラリをインストールします。　※CDNを使う方法もありますが割愛します

```
$ yarn add @editorjs/editorjs
```

pluginsフォルダ（なければ作成)にeditor.jsというファイルを作成し、以下のように記述します。

```JavaScript:./plugins/editor.js
import EditorJS from '@editorjs/editorjs'

export default ({ $axios }, inject) => {
  inject('editor', {
    EditorJS: ({ holder, placeholder, data }) => {
      return new EditorJS({
        holder,
        placeholder,
        data,
      })
    },
  })
}
```

nuxt.config.jsを編集して、作成したファイルプラグインとして呼び出せるようにします。

```JavaScript:nuxt.config.js
//省略

// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
 plugins: [{ src: '~/plugins/editor.js', ssr: false }],
```
`ssr:false`と記述することでクライアントサイドでのみファイルがインクルードされるようにしています。（デフォルトではtrue）
[Nuxt - plugins プロパティ](https://nuxtjs.org/ja/docs/configuration-glossary/configuration-plugins)

次にエディット画面を用意します。
pagesディレクトリ配下にedit.vueファイルを作成します。

```Vue:./pages/edit.vue
<template>
  <div id="editorjs"></div>
</template>

<script>
export default {
  data() {
    return {
      editor: null,
    }
  },
  mounted() {
    this.editor = this.$editor.EditorJS({
      holder: 'editorjs',
      placeholder: 'No content',
      data: {},
    })
  },
}
</script>
```


エディターを描画したい要素のIDを任意に設定し、`holder`に同じように記述します。
今回は`editorjs`にしています。

`placeholder`には何も入力がない状態で表示させる文字を設定します。
今回は`No content`にしています。

`data`には初期表示させるデータを設定します。

これでEditor.jsを使う準備ができました。

editページにアクセスすると
http://localhost:3000/edit


![スクリーンショット 2021-11-17 20.57.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402129/081c7161-290f-8ca7-47d0-1378ed1086e1.png)


マッサラなキャンパスが用意されています。

Notionのようなブロックエディタになっていて、文字の入力と３つのインラインツールが使用できます。
![スクリーンショット 2021-11-17 22.14.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402129/7fe8c2ae-2317-6f8d-a706-e6260bf8cf07.png)

しかしこのままではシンプルな文字が入力できるだけです。

## 拡張

Editor.jsは機能を追加して拡張することで真価を発揮します。

拡張の方法は２つ。

- 既存のプラグインを追加する
- 自作する

まずは前者の方法から。

###  既存のプラグインを追加する
Editor.jsには基本的な機能を追加するためのプラグインがいくつか用意されています。
今回は[Editor.jsのGithub](https://github.com/editor-js)で紹介されている中からいくつか選んで使用します。

まずはモジュールをインストールします。

```
$ yarn add @editorjs/header @editorjs/link @editorjs/quote
```

editor.jsファイルを更新します。

```JavaScript:./plugins.editor.js
import EditorJS from '@editorjs/editorjs'
import Header from '@editorjs/header'
import List from '@editorjs/list'
import Quote from '@editorjs/quote'

export default ({ $axios }, inject) => {
  inject('editor', {
    EditorJS: ({ holder, placeholder, data }) => {
      return new EditorJS({
        holder,
        placeholder,
        data,
        tools: {
          header: {
            class: Header,
            inlineToolbar: true,
            config: {
              levels: [2, 3, 4],
              defaultLevel: 3,
            },
          },
          quote: {
            class: Quote,
            inlineToolbar: true,
          },
          list: {
            class: List,
            inlineToolbar: true,
          },
        },
      })
    },
  })
}
```

`tools`に拡張したい機能を追加していきます。

`header`を例に解説すると

```JavaScript
header: {
  class: Header,
  inlineToolbar: true,
  config: {
    levels: [2, 3, 4],
    defaultLevel: 3,
},
```

- `header`：JSONで出力されるときのtypeプロパティの値になります。
- `class`：importしたHeaderクラスを定義しています。
- `inlineToolbar`：trueにすることで、文字をドラッグしたときにでるインラインツールバーから機能を使うことができるようになります。
- `config`：追加した機能にオプションがある場合に入力します。今回はヘッダータグがデフォルトで`<h3>`タグになるようにしています。

これで拡張ができました。
![スクリーンショット 2021-11-17 22.38.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402129/dc5bee04-8452-a026-1549-6dbf5f887fc7.png)

### 自作する
Editor.jsは任意の機能を追加するために必要なAPIを提供してくれていて、とても簡単に機能追加ができます。

今回はコードを記述するコードブロックを作成します。
pluginsディレクトリ配下にcode_tool.jsファイルを追加します。

```JavaScript:./plugins/code_tool.js
class CodeTool {
    static get toolbox() {
      return {
        title: 'code',
        icon: `<span>Code</span>`,
      }
    }
  
    constructor({ data }) {
      this.data = data
      this.wrapper = undefined
    }
  
    render() {
      this.wrapper = document.createElement('div')
      this.wrapper.classList.add('code-block')
      const pre = document.createElement('pre')
      const code = document.createElement('code')
      const textarea = document.createElement('textarea')
      textarea.value = this.data.code ? this.data.code : ''
  
      this.wrapper.appendChild(pre)
      pre.appendChild(code)
      code.appendChild(textarea)
  
      textarea.addEventListener('input', () => {
        const scrollHeight = textarea.scrollHeight
        textarea.style.height = scrollHeight + 'px'
      })
      return this.wrapper
    }
  
    save(blockContent) {
      const textarea = blockContent.querySelector('textarea')
      const code = textarea.value
      return {
        code,
      }
    }
  }
  
  export { CodeTool }
  

```

各メソッドを解説します。

```JavaScript
 static get toolbox() {
      return {
        title: 'code',
        icon: `<span>Code</span>`,
      }
    }
```
toolboxメソッドではエディタで機能を使用する際の名前とアイコンを設定します。

今回の設定だと画像のようになります。
![スクリーンショット 2021-11-17 22.56.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402129/d0f8dda6-978f-b721-8867-8135b1cffbfc.png)

```JavaScript
constructor({ data }) {
	this.data = data
	this.wrapper = undefined
}
```
constructorメソッドでは初期値として受け取ったdataの中の、ブロック単位のデータを引数として受け取れます。

今回作成するコードでは以下のようなデータなります。

```Json
{
    "id": "Q5whZ_4tYd",
    "data": {
        "code": "const hoge = 'hoge'"
    },
    "type": "code"
}
```

```JavaScript
render() {
	this.wrapper = document.createElement('div')
	this.wrapper.classList.add('code-block')
	const pre = document.createElement('pre')
	const code = document.createElement('code')
	const textarea = document.createElement('textarea')
	textarea.value = this.data.code ? this.data.code : ''

	this.wrapper.appendChild(pre)
	pre.appendChild(code)
	code.appendChild(textarea)

	textarea.addEventListener('input', () => {
	const scrollHeight = textarea.scrollHeight
	textarea.style.height = scrollHeight + 'px'
	})
	return this.wrapper
}

```
render関数ではエディタに描画されるDOM要素を設定します。

```JavaScript
save(blockContent) {
	const textarea = blockContent.querySelector('textarea')
	const code = textarea.value
	return {
		code,
	}
}
```
saveメソッドでは描画された要素のデータを引数として受け取り、出力されるJSONのdataプロパティの内容を戻り値として設定します。
codeプロパティの値がtextreaに入力された値になるように設定しています。


次に作成したCodeToolを読み込みます。

```JavaScript:./plugins/editor.js
import EditorJS from '@editorjs/editorjs'
// 省略
import { CodeTool } from './code_tool'

export default ({ $axios }, inject) => {
  inject('editor', {
    EditorJS: ({ holder, placeholder, data }) => {
      return new EditorJS({
        holder,
        placeholder,
        data,
        logLevel: 'ERROR',
        minHeight: 50,
        tools: {
        // 省略
          code: CodeTool,
        },
      })
    },
  })
}
```

これで自作したCodeToolが使えるようになりました。
このままではコードを記述する機能としては不十分ですが、今回はよしとします。

editページで確認すると画像のようにコードブロックっぽいだけのtextarea要素に記述ができるようになっています。。

![スクリーンショット 2021-11-17 23.22.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402129/4958f318-2e59-3081-62b2-bacc5244c07a.png)

## データ管理

このままではタブを閉じたりリロードすると入力した内容は消えてしまうので、保存と更新ができるようにします。

Editor.jsはデータをJSON形式で出力するため、簡単にデータのやり取りができます。
基本的にデータの加工が必要ないので、データの保管をする収納ケースさえあれば十分です。

今回は、 記事の内容を文字列で取得/更新できるAPIがあると仮定します。

edit.vueファイルにasyncDataメソッドを追加して、記事のデータを取得します。

```Vue:./pages/edit.vue
<script>
export default {
  asyncData(context) {
    return context.$axios
      .get('post')
      .then((response) => {
        return {
          post: response,
        }
      })
      .catch((error) => {
        console.log(error)
      })
  },
// 省略
}
</script>
```

そして、取得した記事データを初期値として設定します。

```Vue:./pages/edit.vue
<script>
export default {
// 省略
  mounted() {
    this.editor = this.$editor.EditorJS({
      holder: 'editorjs',
      placeholder: 'No content',
    - data: {},
    + data: JSON.parse(this.post),
    })
  },
// 省略
}
</script>
```


これで取得したデータを初期値として表示できるようになりました。

次に編集した内容で更新できるようにします。

Editor.JSではsaveメソッドを使ってデータの保存をすることができます。
edit.vueファイルを更新します。

```Vue:./pages/edit.vue
<template>
  <div>
    <div id="editorjs"></div>
    <button @click="save()">更新</button>
  </div>
</template>

<script>
export default {
// 省略
  methods: {
    async save() {
      await this.editor.save().then((savedData) => {
        this.post = JSON.stringify(savedData)
      })
      this.$axios
        .put('post', {
          post: this.post,
        })
        .catch((err) => {
          console.log(err)
        })
    },
  },
}
</script>
```
EditorJSのsaveメソッドはエディタに記述された内容をJSON形式のデータで出力してくれます。
今回はsaveメソッドで出力されたJSONオブジェクトを文字列形式に変換しています。

更新ボタンをクリックすることで記事の更新が可能になりました。

これでテキストエディタとして標準的な機能は揃いました。

## さいごに
Editor.jsを使ってテキストエディタを実装する方法を解説しました。
自分自身がエディタを実装する際に出会って、いいな〜って思ったので記事にしました。
本記事では解説してませんが、インラインツールの拡張等もEditor.jsでは可能なのでぜひ使ってみてください。