DEV Community

Sharon Rachel Levin
Sharon Rachel Levin

Posted on

Setting up a rich text editor in React: Inserting images

My text editor set up using CKEditor with image upload functionality

Hello all! In my previous post I went over how to set up a very basic rich text editor using CKEditor 5. The Classic Build that I used comes with the option to allow inserting images, but not out of the box.

If you try to upload an image, you will get this warning in the console along with a link to the docs:

filerepository-no-upload-adapter: Upload adapter is not defined.

Reading the description of the error in the link, you'll see that you need to configure an 'upload adapter' in order to get usage out of the image uploading feature.

You need to enable an upload adapter in order to be able to upload files. This warning shows up when FileRepository is being used without definining an upload adapter.

In this post I'll show you how I set up my own upload adapter in order to use the image uploading feature in CKEditor.

Set up the custom upload adapter

First I'm setting up an UploadAdapter class. Inside of our constructor we need to a have a file loader instance that will control the process of reading and uploading files.

import { firebase } from '../utilities/firebase'

class UploadAdapter {
    constructor(loader) {
        this.loader = loader
    }
}

export default UploadAdapter

Our upload adapter class also needs to have an upload() method that returns a promise that is resolved when the file is successfully uploaded.

To make my code a little more dry, I created a variable for my firebase reference called uploadTask. Then I'm using uploadTask.on() to ultimately handle what I want to happen if the image is successfully uploaded to my firebase storage.

In .on() we're passing in 4 parameters:

  • a string that identifies an event (for us, we're checking if the state changed with firebase.storage.TaskEvent.STATE_CHANGED)
  • a callback function that gives you access to a snapshot
  • a second callback for error handling
  • a third callback for if the file upload is successful

Here's what my upload() method looks like:

upload() {
    return this.loader.file.then(
        (file) =>
            new Promise((resolve, reject) => {
                // firebase reference
                let uploadTask = firebase.storage().ref()
                    .child(`images/${file.name}`)
                    .put(file)

                uploadTask.on(
                        firebase.storage.TaskEvent.STATE_CHANGED,
                        (snapshot) => {
                            /* snapshot with info about 
                            the upload progress & metadata */
                        },
                        (error) => {
                            // error handling
                        },
                        () => {
                            // upload successful
                        }
                    )
            })
    )
}

The snapshot contains info about the upload progress and other metadata, which can be used if you'd like to display a progress bar to the user. You can use the error callback to handle any errors that may occur during the upload process.

Finally, we want to tell firebase what to do if the image upload is successful. In our last callback function, we want to grab the download URL that was created and tell CKEditor to use that URL to display the image within the editor.

uploadTask.snapshot.ref
    .getDownloadURL()
    .then((downloadURL) => {
        resolve({
            default: downloadURL
        })
    })

Ultimately, your UploadAdapter file should end up looking like this:

import { firebase } from '../utilities/firebase'

class UploadAdapter {
    constructor(loader) {
        this.loader = loader
    }
    upload() {
        return this.loader.file.then(
            (file) =>
                new Promise((resolve, reject) => {
                    // firebase reference
                    let uploadTask = firebase.storage().ref()
                        .child(`images/${file.name}`)
                        .put(file)

                    uploadTask.on(
                        firebase.storage.TaskEvent.STATE_CHANGED,
                        (snapshot) => {
                            /* snapshot with info about 
                            the upload progress & metadata */
                        },
                        (error) => {
                            // error handling
                        },
                        () => {
                            // upload successful
                            uploadTask.snapshot.ref
                                .getDownloadURL()
                                .then((downloadURL) => {
                                    resolve({
                                        default: downloadURL
                                    })
                                })
                        }
                    )
                })
        )
    }
}

export default UploadAdapter

Import our custom upload adapter into our CKEditor

Import UploadAdapter into your text editor component where you are using CKEditor. The CKEditor component takes a prop called onInit, a function called when the editor is initialized. This gives us access to the initialized editor.

To specify that we want to use our own custom upload adapter, we want to utilize the FileRepository plugin. FileRepository gives us the prop .createUploadAdapter, which is a function that takes in a file loader instance and returns a new upload adapter instance.

import React, { useState } from 'react'
import CKEditor from '@ckeditor/ckeditor5-react'
import ClassicEditor from '@ckeditor/ckeditor5-build-classic'
import UploadAdapter from './UploadAdapter'

const TextEditor = ({ onSubmit }) => {
    const [body, setBody] = useState('')

    const handleSubmit = (e) => {
        e.preventDefault()
        onSubmit({ body })
    }

    return (
        <form onSubmit={handleSubmit}>
            <CKEditor
                // here's where we are using our custom adapter
                onInit={(editor) => {
                    editor.plugins.get('FileRepository')
                        .createUploadAdapter = (loader) => {
                            return new UploadAdapter(loader)
                        }
                }}
                editor={ClassicEditor}
                onChange={(event, editor) => {
                    const data = editor.getData()
                    setBody(data)
                }}
            />
            <button type='submit'>Submit</button>
        </form>
    )
}

export default TextEditor

Now you should be allowed to upload images in your editor. Out of the box it allows you to upload, drag and paste images into the editor. It also gives you the option to let your images be inline or block, and include an image caption.

Thanks for reading! 😊

Discussion (3)

Collapse
tutran34349129 profile image
Tu Tran • Edited on

Great Article. I have a question about how to delete the image uploaded in firebase when users decide not to use that image in the editor. Do we have an onChange event that triggers when the user hits BackSpace (to remove image)? Or we have to go to the firebase and remove it manually.

Collapse
anildharni profile image
Anil Dharni

This helped me a lot. But could you please me out with what to do when a user uploads an image on editor and then decides to discard that image. Now this image is being stored on the firebase storage. How to delete the images in such a case?

Collapse
udayupix profile image
udayupix

Nice...
Please provide git repos also...
It will be helpful..
THANKS IN ADVANCE....