Laravel × Vue 環境で VueQuill の導入

VueQuillはVue3用のリッチテキストエディタを簡単に実装できるコンポーネントです。

さらに quill-image-uploaderを使用することで、画像を Base64ではなくサーバーにアップロードできます。
このガイドでは、以下の内容を説明します

- VueQuill と Quill-image-uploader のセットアップ
- Laravel を用いた画像アップロード機能の構築
- Vue コンポーネントの構成例
- Laravel コントローラーの処理例

## 1. 必要なパッケージのインストール
以下のパッケージをインストールします。
```terminal:terminal
npm install @vueup/vue-quill@latest --save
npm install quill-image-uploader --save
```

https://vueup.github.io/vue-quill/

## 2. VueQuill のセットアップ
```vue:QuillEditor.vue
<template>
<div>
    <div>
    <QuillEditor
        v-model:content="localContent"
        :options="editorOptions"
        contentType="html"
        @update:content="handleContentChange"
    />
    </div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'

import Quill from 'quill'
import { QuillEditor } from '@vueup/vue-quill'
import QuillImageUploader from 'Quill-image-uploader'

import axios from 'axios';

Quill.register('modules/imageUploader', QuillImageUploader)

import '@vueup/vue-quill/dist/vue-quill.snow.css';

const props = defineProps({
    initialContent: {
        type: String,
        default: ''
    },
    content: {
        type: String,
        default: ''
    },
    uploadUrl: {
        type: String,
        default: '/hogehoge'
  }
})
const emit = defineEmits([
    'update:content',
    'image-uploaded'
])
const localContent = ref(props.initialContent)


const editorOptions = {
    theme: 'snow',
    modules: {
     toolbar: {
		  //ツールバーカスタム可能
	      container: [['bold', 'italic', 'underline'], ['image']],
	    }
	    //画像をstorageに保存処理　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　
        imageUploader: {
            upload: async (file) => {
                try {
                    const formData = new FormData();
                    formData.append('image', file);
                    const response = await axios.post(props.uploadUrl, formData, {
                    });
                    if (response.data && response.data.filePath) {
                        emit('image-uploaded', {
                            fileName: file.name,
                            filePath: response.data.filePath
                        });
                        return response.data.filePath;
                    }
                } catch (error) {
                    console.error('画像アップロードエラー:', error);
                    throw error;
                }
            }
        }
    }
}

// コンテンツ更新時のハンドラー
const handleContentChange = (newContent) => {
    emit('update:content', newContent)
}

watch(() => props.content, (newContent) => {
    if (newContent !== localContent.value) {
        localContent.value = newContent
    }
})

onMounted(() => {
   // 初期値がある場合は設定
  if (props.initialContent) {
    localContent.value = props.initialContent
  }
})
</script>
```
ツールバーのカスタム参考サイト

https://quilljs.com/docs/modules/toolbar/

## 3. エディターをフォームに統合
```vuejs:Form.vue
<template>
    <div>
        <QuillEditor 
          v-model:content="editorContent" 
          :initial-content="initialContent"
          @update:content="handleContentUpdate" 
          @image-uploaded="handleImageUpload"/>

    <div>
        <v-btn @click.prevent="handleCancel">
            キャンセル
        </v-btn>

        <v-btn. click.prevent="onClick">
            作成
        </v-btn>
    </div>
			</template>

<script setup lang="ts">
import { ref, computed, PropType } from 'vue'

import { router } from '@inertiajs/vue3';

import QuillEditor from '@/Components/QuillEditor.vue'

import '@vueup/vue-quill/dist/vue-quill.snow.css';
import 'quill/dist/quill.snow.css';

const props = defineProps({
    contents: {
        type: String as PropType<string | null>,
        default: ''
    }
})
// 初期値を計算する
const initialContent = computed(() => {
    return props.contents ?? ''
})

const editorContent = ref<string | null>(props.contents ?? null)
const uploadedImages = ref<Array<{ fileName: string, filePath: string }>>([])

// キャンセルボタンを押した時の処理
const handleCancel = () => {
    editorContent.value = props.contents ?? ''
}

const handleImageUpload = (imageData: { fileName: string, filePath: string }) => {
    uploadedImages.value.push(imageData)
}
const handleContentUpdate = (newContent) => {
    editorContent.value = newContent
}
const onClick = () => {
    router.post(route('post先URL'), {
        content: editorContent.value ?? '',
    })
}
</script>
```
## 4. Laravel サイドの処理
```php:EditorController
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class EditorController extends Controller
{
    public function edit()
    {
     $contents = EditorContent::first();
        return Inertia::render('Editor', ['contents' => $contents->content]);
    }

    public function uploadImage(Request $request)
    {
        $request->validate(['image' => 'required|image|mimes:jpeg,png,jpg,gif']);

        if ($request->hasFile('image')) {
            $file = $request->file('image');
            $fileName = uniqid() . '.' . $file->getClientOriginalExtension();
            $path = $file->storeAs('images/hoge', $fileName, 'public');
						
			//ファイル名をvueにreturn
            return response()->json(['filePath' => Storage::url($path)]);
        }

        return response()->json(['error' => 'アップロードに失敗しました'], 400);
    }

    public function create(Request $request)
    {
		//updateOrCreate
        EditorContent::updateOrCreate(['key' => hoge], ['content' => $request->content]);
        return redirect()->route('editor.edit');
    }
}
```
imageUploaderはプレーンなjsonを受け取りファイル名をbass64から受け取ったファイル名に上書きするみたいなのでaxiosを使用。
(勉強中のため他に良い方法があるかも)

## 5. エディターコンテンツを表示
```vuejs:index.vue
<template>
  <div class="ql-editor" v-html="content"></div>
</template>

<script setup>
    import '@vueup/vue-quill/dist/vue-quill.snow.css';
</script>
```
## 6.終わり
VueQuillは簡単に導入できるが、画像がbass64にエンコードされDBが見づらくなるので解決するために、quill-image-uploaderを使用してみました。
画像は挿入するだけで、配置やサイズの変更は出来ないのでモジュール追加が必要です。
参考になれば幸いです。