Very often you will find yourself in a need of a good rich text editor that supports everything out of the box. However, usually, this is not the case, and you either find a new editor to try out or implement the missing features yourself.
Image upload in rich text editors is not always behaving as you want it. Sometimes it stores images in base64 which substantially increases image size and can hinder performance, sometimes it requires an additional paid plugin by the creator, and sometimes it is completely left out.
In this article, we will look into how we can set up some easy image upload to our server from a rich text editor.
We will utilize a couple of packages.
- Spatie Laravel-medialibrary - https://github.com/spatie/laravel-medialibrary
- Surmon China Vue Quill Editor - https://github.com/surmon-china/vue-quill-editor
- Quill Resize Module - https://github.com/kensnyder/quill-image-resize-module
At this point, it would be a good time to install Laravel and Vue dependencies if you haven’t already.
For our example, we will have an article model with a single database field called body. We will create our migration, routes, controller, and views.
First, we create a new migration
php artisan make:migration create_articles_table
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->text(‘body’);
$table->timestamps();
});
}
In order to create the table, we need to migrate this class
php artisan migrate
We also need an Article model.
class Article extends Model
{
protected $fillable = [
'content',
];
}
Let’s create ArticleController swiftly with artisan command
php artisan make:controller ArticleController
Now we will create three methods and three routes that can be called on the controller.
class ArticleController extends Controller
{
public function create()
{
}
public function store(Request $request)
{
}
public function show(Request $request)
{
}
}
And our routes
Route::get('/article', ArticleController@create')->name('article.create');
Route::post('/article/create', ArticleController@store')->name('user.store');
Route::get('/article/{article}', ArticleController@show')->name('article.show');
Let's quickly create our view files. We will create a pages folder inside our views with an Article folder and two blade files: create and show. In create we will place our rich text editor and in the show, we will display saved article from the database.
Let's update our create() method to return the proper view.
public function create()
{
return view('pages.article.create');
}
At this point, we will install vue-quill-editor and image resize module.
npm install vue-quill-editor –save
npm install quill-image-resize
Now, let’s create a new Vue component called ArticleCreate. Here, we will import quill editor.
In the script part of the component lets import and configure quill editor.
<script>
// Import Quill required dependencies
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor, Quill } from 'vue-quill-editor'
import ImageResize from 'quill-image-resize';
// Register ImageResize module
Quill.register('modules/imageResize', ImageResize);
// Set toolbar options
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{'header': 1}, {'header': 2}],
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'sub'}, {'script': 'super'}],
[{'indent': '-1'}, {'indent': '+1'}],
[{'direction': 'rtl'}],
[{'size': ['small', false, 'large', 'huge']}],
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}],
[{'font': []}],
[{'align': []}],
['link', 'image', 'video'],
['clean'],
];
export default {
components: {
quillEditor
},
data() {
return {
form: {
body: ''
},
editorOption: {
modules: {
toolbar: {
container: toolbarOptions,
},
history: {
delay: 1000,
maxStack: 50,
userOnly: false
},
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
}
}
},
}
},
methods: {
onEditorBlur(editor) {
// console.log('editor blur!', editor)
},
onEditorFocus(editor) {
// console.log('editor focus!', editor)
},
onEditorReady(editor) {
// console.log('editor ready!', editor)
},
submitForm(){
}
},
computed: {
editor(){
return this.$refs.myQuillEditor.quill
}
},
}
</script>
And let’s add HTML code to our template.
<template>
<div>
<label for="rich-text" class="form-label">Content</label>
<quill-editor v-model="form.body"
class="mb-3"
id="rich-text"
rows="20"
:options="editorOption"
ref="myQuillEditor"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)">
</quill-editor>
</div>
</template>
In this large chunk of code, we have done the following. We have imported Vue Quill and Image Resize module that we installed. We set the toolbar options for Quill, registered the Image Resize module, and told Quill to use the configurations we just created.
So now save the code and visit http://127.0.0.1:8000/article.
So far it looks good. Seems like our code is working just fine.
Now let’s make a pause in writing code to think about how we are going to make this image upload work. Here is the idea.
We create a custom image handler for the image upload menu in Quill Toolbar. We also create a hidden file type input somewhere in our template which will not be visible to the user.
Then, whenever the user clicks on the upload image button, we prevent the default behavior and instead activate our own file input and let the user select an image.
After the user selects an image, we take the image, convert it to base64, push the base64 string to images array in our data(), we take the current cursor position and insert the image tag with base64 string as the <img src=”base64” /> to quill editor.
After the user saves the article, in the backend we will go through base64 strings in the images array. For every string in the array, with the help of Spatie Media Library, we will create an image file and then replace in the article content the <img src=”base64” /> with the full URL of the newly created image <img src=”url”>.
After we laid out the concept let’s jump to implementing it.
First, let’s add a handler to Quill toolbar configuration
toolbar: {
container: toolbarOptions,
handlers: {
'image': function (value) {
if (value) {
document.querySelector('#imageUpload').click();
} else {
this.quill.format('image', false);
}
}
},
},
and let’s create an input with file type and place it beneath Quill editor.
<div class="custom-file d-none">
<input ref="image" @change="imageUpload($event)" type="file" class="custom-file-input" id="imageUpload" aria-describedby="imageUploadAddon">
<label class="custom-file-label" for="imageUpload">Choose file</label>
</div>
We have to add images array to form in data()
Add imageUpload method
imageUpload(e) {
if (e.target.files.length !== 0) {
let quill = this.editor;
let reader = new FileReader();
reader.readAsDataURL(e.target.files[0]);
let self = this;
reader.onloadend = function() {
let base64data = reader.result;
self.form.images.push(base64data);
// Get cursor location
let length = quill.getSelection().index;
// Insert image at cursor location
quill.insertEmbed(length, 'image', base64data);
// Set cursor to the end
quill.setSelection(length + 1);
}
}
},
From the frontend side, the only thing left to do is to create a save button and to make an ajax request to our backend.
Our button
<button @click="submitForm" class="btn btn-sm btn-success">Save</button>
And our method
submitForm(){
axios.post('/article/create',
{ body: this.form.body, images: this.form.images })
.then(response => {
console.log("Response", response)
})
.catch(error => {
console.log("Error", error)
})
}
For our backend, we first need to install Spatie Media Library and associate it with Article model.
composer require "spatie/laravel-medialibrary:^7.0.0"
We will publish the migration files
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations"
and run the migrations with
php artisan migrate
If you run into issues while migrating change the type of following json fields to text.
$table->text('manipulations');
$table->text('custom_properties');
$table->text('responsive_images');
Now let’s configure our Article model.
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia\HasMedia;
use Spatie\MediaLibrary\HasMedia\HasMediaTrait;
class Article extends Model implements HasMedia
{
use HasMediaTrait;
protected $fillable = [
'body',
];
}
Finally, everything is ready to implement the final code to create and replace images.
public function store(Request $request)
{
$body = $request->body;
$images = $request->images;
// Create a new article
$article = Article::create([
'body' => $body
]);
// If images not empty
if ($images) {
foreach ($images as $image)
{
// Create a new image from base64 string and attach it to article in article-images collection
$article->addMediaFromBase64($image)->toMediaCollection('article-images');
// Get all images as we will need the last one uploaded
$mediaItems = $article->load('media')->getMedia('article-images');
// Replace the base64 string in article body with the url of the last uploaded image
$article->body = str_replace($image, $mediaItems[count($mediaItems) - 1]->getFullUrl(), $article->body);
}
}
$article->save();
return response()->json('Success');
}
Let’s add some text and an image to our text editor.
We will now finish our method for showing a single article.
public function show(Request $request, Article $article)
{
return view('pages.article.show', compact('article'));
}
Our show.blade.php file.
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header">Article</div>
<div class="card-body">
{!! $article->body !!}
</div>
</div>
</div>
</div>
</div>
Once we click on the save button let’s visit our show route http://127.0.0.1:8000/article/1
Sure enough, we have our text and the image we uploaded. Awesome.
Hope you liked this simple image upload. Subscribe to the newsletter if you would like to get a notification when we publish similar articles.
Stay awesome and happy coding.