Laravel5.5＋Vue.js環境でCKEditorを使ってみる

# 環境

わけあって PHP 7.0 環境なので Laravel 5.5 です。
基本は変わらないと思います。
動作確認用のコードなので適当なところが多いです。

- Laravel 5.5
- vue.js 2.5.7
- ckeditor5-build-classic 12.2.0
- ckeditor5-vue 1.0.0-beta.2

まだβなのでできれば Vue.js 環境では使わないほうが良いのかもしれない…

```
GET /api/news
POST /api/news
POST /api/images
```

Laravelには上記の [API](https://readouble.com/laravel/5.5/ja/passport.html) を定義しています。

# vue用のやつをインストール

```sh:
npm install --save @ckeditor/ckeditor5-vue @ckeditor/ckeditor5-build-classic
```

## app.js に追記

```js:app.js
window.CKEditor = require('@ckeditor/ckeditor5-vue');
Vue.use(CKEditor);
Vue.component('ckeditor-component', require('./components/CKEditorComponent.vue'));
```

`ckeditor-component` は今回用につくるです。

# 日本語化

入れてみるとデフォルトが英語だったので日本語化します。
してって言われたのでします。

```js:app.js
require('@ckeditor/ckeditor5-build-classic/build/translations/ja');
```

こちらも `app.js` に追記します。
完全ではないのでCDNとかのほうが良いかも？
グローバル変数を使って国際化しているっぽいので、読み込むだけでOKです。

# コンポーネントの作成

```vue:CKEditorComponent.vue
<template>
    <div>
        <ckeditor
            :editor="editor"
            v-model="editorData"
            :config="editorConfig"
        ></ckeditor>
    </div>
</template>

<script>
    import ClassicEditor from '@ckeditor/ckeditor5-build-classic';

    export default {
        name: 'ckeditor-component',
        data() {
            return {
                editor: ClassicEditor,
                editorData: '',
                editorConfig: {
                    language: 'ja'
                }
            };
        }
    }
</script>
```

サンプルそのままですので特筆すべきところもないですね。

# bladeにコンポーネントを追加

```php:ckeditor.blade.php
<!doctype html>
<html lang="{{ app()->getLocale() }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel</title>

        <link rel="stylesheet" href="{{ mix('/css/app.css') }}">
        <meta name="csrf-token" content="{{ csrf_token() }}">
    </head>
    <body>
        <div id="app">
            <ckeditor-component></ckeditor-component>
        </div>
        <script src="{{ mix('js/app.js') }}"></script>
    </body>
</html>
```

CKEditor関係ないですが、注意点は２点です。

```html:
<meta name="csrf-token" content="{{ csrf_token() }}">
<script src="{{ mix('js/app.js') }}"></script>
```

POST用のCSRFトークンと `mix` です。
この辺はLaravelの機能ですね。

実際にVueを初期化しているのは `resouces/assets/js/app.js` です。
`laravel-mix` によって public配下に移動・コンパイルする定義が書かれている模様。

# 内容を保存する

[Autosave](https://ckeditor.com/docs/ckeditor5/latest/api/module_autosave_autosave-Autosave.html) がうまいこと動かなかったので `@input` を使って保存します。

APIの認証にトークンが必要なので `axios` に埋め込みます。

```js:CKEditorComponent.vue
const token = '***';
const apiClient = axios.create({
    baseURL: '/api/',
    headers: {'Authorization': `Bearer ${token}`}
});
```

保存用の処理を `methods` に追加。

```js:CKEditorComponent.vue
onEditorInput() {
    const postData = new URLSearchParams();
    postData.append('content', this.editorData);
    apiClient.post('news', postData).then(response => {});
}
```

勝手に保存されてるのがいいという要望のもとこういう形になっています。
ボタンとかで保存する場合はふつうに `editorData` をPOSTしてやれば良いですね。

# 画像のアップロード

自前のサーバにアップロードするのでプラグインなしでやります。
向き先変えるだけでいけると思いますけど簡単そう・後々のことを考えて。

## Adapterの作成

[ここ](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html#the-anatomy-of-the-adapter) にある通りつくります。
とりあえずはデフォルトの画像だけ…

```js:MediaUploadAdapter.js
export default class MediaUploadAdapter {
    constructor(loader, token) {
        this.loader = loader;
        this.token = token;
    }

    upload() {
        const token = this.token;
        return this.loader.file.then(uploadedFile => {
            return new Promise((resolve, reject) => {
                const formData = new FormData();
                formData.append('upload', uploadedFile);
                axios({
                    url: '/api/images',
                    method: 'post',
                    formData,
                    headers: {
                        'Content-Type': 'multipart/form-data;',
                        'Authorization': `Bearer ${token}`,
                    },
                    withCredentials: false
                }).then(response => {
                    if (response.data.result === 'success') {
                        resolve({default: response.data.url});
                    } else {
                        reject(response.data.message);
                    }
                }).catch(error => {
                    // console.log(error.response.headers);
                    console.log(error.response.status, error.response.data.message);
                });
            });
        });
    }

    abort() {
    }
}
```

## 登録する

作ったアダプタをエディタに登録します。
これもドキュメント通りにやっています。

```js:CKEditorComponent.vue
export default {
    data() {
        return {
            editor: ClassicEditor,
            editorData: '',
            editorConfig: {
                language: 'ja',
                extraPlugins: [this.uploadAdapter]
            }
        };
    },
    methods: {
        uploadAdapter(editor) {
            editor.plugins.get('FileRepository').createUploadAdapter = loader => {
                return new MediaUploadAdapter(loader, token);
            };
        }
    }
}
```

# 動画の埋め込み（途中まで）

動画を画像のように埋め込みたいという要望があるので調べるだけ調べました。
あまり頻度は高くないとのことだったので、途中までです。

## 1. mp4のドラッグ＆ドロップを検知する

アダプタを追加したときのようにプラグインを追加します。

```js:CKEditorComponent.vue
export default {
    data() {
        return {
            editor: ClassicEditor,
            editorData: '',
            editorConfig: {
                language: 'ja',
                extraPlugins: [this.handleDropEvent]
            }
        };
    },
    methods: {
        handleDropEvent(editor) {
            editor.editing.view.document.on('drop', (event, data) => {
                if (data.dataTransfer.files.length > 0) {
                    data.dataTransfer.files.forEach(file => {
                        if (file.type === 'video/mp4') {
                            event.stop();
                            data.preventDefault();
                            // ここでファイルのアップロード処理＆ローディング埋め込み
                            // 同時にファイルをエディタに埋め込む処理を記載する
                        }
                    });
                }
            }, {priority: 'high'});
        }
    }
}
```

## 2. エディタ内に埋め込む

エディタに埋め込むには対象と場所を指定してやる必要があります。
この辺から CKEditor のマニュアルとコードとにらめっこしながらすすめています。

drop イベント内で取れる `Position` は View モジュールのものなので、
`Writer` に渡してやるために Model の `Position` に変更する必要があります。
そのためのメソッドが `Mapper` に用意されており、そいつは `EditorController` に生えていますので、それを使ってエディタ内に埋め込みします。

```js:
editor.model.change(writer => {
    const insertPosition = editor.editing.mapper.toModelPosition(data.dropRange.end);
    // ここでローディング画像を作る
    const element = writer.createElement();
    // こんな感じで埋め込む
    writer.insert(element, insertPosition);
});
```

アップロードが完了するまではローディングのgifでも読み込ませておいて、
アップロードが完了後にEmbedを `Writer` の `create*` で作るとは思うのですが、調査に時間がかかりそうだったので、一旦ここで動画のアップロードは後日にしようということになりました。
ので、この記事もここまでとなります。中途半端ですみません。

## 3. ファイルをサーバにアップロードする

ここは普通に axios でアップロードするだけです。（割愛）