Nuxt.js + Editor.js + Laravel(API)でお好みエディタ作成

# 背景
記事作成機能作る必要があり、作成、保存、描画をどうしようかと思っていたときに、block-styled editorなるものがあったので使ってみた。
[Editor.js](https://editorjs.io/)
# Editor.jsのここがよかった
 *　Jsonでかつ、ブロック単位で分かれているため扱いやすい。
 *　プラグインが優秀
 *　描画・修飾をフロントに任せられる

# 下準備
Laravel6系
Nuxt2.12.2(SSR)

```terminal
npm i @editorjs/editorjs --save
```

```vuejs

<template>
  <div>
    <div id="editorjs"/>
    <v-btn @click="save">保存</v-btn>
  </div>
</template>

<script>
  let EditorJS = null;
  if (process.client) {
    EditorJS = require('@editorjs/editorjs');
  }

  export default {
    name: "ArticleCreateForm",
    data: () => ({
      editor: {},
      article: {},
    }),
    mounted() {
      this.editor = new EditorJS({
        holder: 'editorjs',    
      }),
    methods: {
      save() {
        this.editor.save().then((data) => {
          this.article = data
        }).catch(err => {
          console.log(err)
        })
      },
    }
  }
</script>

<style scoped>

</style>

```
SSRのときはEditorjsを読み込む際に、Windowエラーが出るため、クライアントで読み込ませるようにした。

 これだけで、とりあえずはParagraphをブロックごとに記述できる。

# プラグインの導入
画像の埋め込みやリンクの埋め込み、ヘッダー、リストなど基本的に使いそうなプラグインをそれぞれ追加することで自分好みにカスタマイズ可能。なんならプラグインは自作も可能。導入したプラグインをそれぞれ簡単に紹介する。

紹介するプラグイン一覧

 * [Header](https://github.com/editor-js/header)(ヘッダー)
 * [EmbedLink](https://github.com/editor-js/link)(埋め込みリンク)
 * [Image](https://github.com/editor-js/image)(画像)
 * [Checklist](https://github.com/editor-js/checklist)(チェックリスト)
 * [List](https://github.com/editor-js/list)(リスト)
 * [Embed](https://github.com/editor-js/embed)(サービス埋め込み)
 * [Quote](https://github.com/editor-js/quote)(引用)
 * [Delimiter](https://github.com/editor-js/delimiter)(区切り)

# 完成形

```terminal
npm i --save @editorjs/header
npm i --save-dev @editorjs/link
npm i --save-dev @editorjs/image
npm i --save-dev @editorjs/checklist
npm i --save @editorjs/list
npm i --save-dev @editorjs/embed
npm i --save-dev @editorjs/quote
npm i --save-dev @editorjs/delimiter
```

```vue
<template>
  <div class="container-fluid">
    <div class="row">
      <div class="col-xs-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
        <div class="card">
          <div class="card-header">記事作成</div>
          <div class="card-body">
            <client-only placeholder="loading">
              <div id="editorjs"/>
            </client-only>
          </div>
          <div class="card-footer">
            <div class="row justify-content-end">
              <v-btn text @click="save">保存</v-btn>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  let EditorJS = null;
  let Header = null;
  let LinkTool = null;
  let ImageTool = null;
  let CheckList = null;
  let List = null;
  let Embed = null;
  let Quote = null;
  let Delimiter = null;

  if (process.client) {
    EditorJS = require('@editorjs/editorjs');
    Header = require('@editorjs/header');
    LinkTool = require('@editorjs/link');
    ImageTool = require('@editorjs/image');
    CheckList = require('@editorjs/checklist');
    List = require('@editorjs/list');
    Embed = require('@editorjs/embed');
    Quote = require('@editorjs/quote');
    Delimiter = require('@editorjs/delimiter');
  }

  export default {
    name: "ArticleCreateForm",
    data: () => ({
      editor: {},
      article: {},
    }),
    mounted() {
      let me = this
      this.editor = new EditorJS({
        holder: 'editorjs',
        tools: {
          header: {
            class: Header,
            shortcut: 'CMD+SHIFT+H',
            config: {
              placeholder: 'ヘッダー',
              levels: [1, 2, 3, 4],
              defaultLevel: 3,
            },
          },
          linkTool: {
            class: LinkTool,
            config: {
              endpoint: 'http://localhost:3000/api/blog/fetch_url',
            }
          },
          image: {
            class: ImageTool,
            config: {
              uploader: {
                uploadByFile(file) {
                  let formData = new FormData()
                  formData.append('image', file)
                  let config = {headers: {'content-type': 'multipart/form-data'}}
                  return me.$axios.post('api/blog/upload_file', formData, config)
                    .then(res => {
                      return res.data
                    })
                },
                //only work when url has extensions like .jpg
                uploadByUrl(url) {
                  return me.$axios.post('api/blog/fetch_file', {url: url}).then(res => {
                    return res.data
                  })
                }
              }
            }
          },
          checklist: {
            class: CheckList,
            inlineToolbar: true,
          },
          list: {
            class: List,
            inlineToolbar: true,
          },
          embed: {
            class: Embed,
            config: {
              services: {
                youtube: true,
                twitter: true,
                instagram: true
              }
            }
          },
          quote: {
            class: Quote,
            inlineToolbar: true,
            shortcut: 'CMD+SHIFT+O',
            config: {
              quotePlaceholder: 'Enter a quote',
              captionPlaceholder: 'Quote\'s author',
            },
          },
          delimiter: Delimiter,
        }
      })
    },
    methods: {
      save() {
        this.editor.save().then((data) => {
          this.article = data
        }).catch(err => {
          console.log(err)
        })
      },
    }
  }
</script>

<style scoped>

</style>

```

# 埋め込みリンク
```vuejs
linkTool: {
  class: LinkTool,
    config: {
      endpoint: 'http://localhost:3000/api/blog/fetch_url',
    }
},
```
エンドポイントを指定する必要あり。今回はnode serverに飛ばしnuxt.config.jsでリバースプロキシ。
ソースを見た感じ、GET methodでリクエストボディに{url: 'https:://...'}的な形で入っていたので、それ用のAPIを実装して、urlからメタ情報を取ってきて規定された形式(ドキュメント参照)でレスポンスを返す。

# 画像挿入
```vuejs

image: {
  class: ImageTool,
    config: {
        uploader: {
          uploadByFile(file) {
            let formData = new FormData()
            formData.append('image', file)
            let config = {headers: {'content-type': 'multipart/form-data'}}
            return me.$axios.post('api/blog/upload_file', formData, config)
              .then(res => {
                  return res.data
                })
             },
             //only work when url has extensions like .jpg
             uploadByUrl(url) {
               return me.$axios.post('api/blog/fetch_file', {url: url}).then(res => {
                 return res.data
                })
              }
             }
          }
        },

```
画像の挿入は正規表現にマッチするurlをエディタ上にペーストしたとき、ローカルからファイルを選択したとき、ドラッグアンドドロップしたときに発火する。
configでエンドポイントだけ設定することも可能だが、axiosを使いたかったのでカスタムアップロードメソッドを定義。thisのスコープがずれるので、適切な場所で`const me = this`をしておく。
こちらもバックエンドを実装、規定された形式でレスポンスを返す。

# おまけ
Laravelのマイグレーションファイルで、エディタで生成したJsonを保存するカラムのタイプをJsonにして、モデルでJsonをキャストするようにしておくと利便性高い。