How to enable image upload support in CKEditor 5?

Yes, image upload is included in all the available builds. In order to make it work, though, you need to configure one of the existing upload adapters or write your own. In short, upload adapter is a simple class which role is to send a file to a server (in whatever way it wants) and resolve the returned promise once it's done.

You can read more in the official Image upload guide or see the short summary of the available options below.

Official upload adapters

There are two built-in adapters:

  • For CKFinder which require you to install the CKFinder connectors on your server.

    Once you have the connector installed on your server, you can configure CKEditor to upload files to that connector by setting the config.ckfinder.uploadUrl option:

    ClassicEditor
        .create( editorElement, {
            ckfinder: {
                uploadUrl: '/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files&responseType=json'
            }
        } )
        .then( ... )
        .catch( ... );
    

    You can also enable full integration with CKFinder's client-side file manager. Check out the CKFinder integration demos and read more in the CKFinder integration guide.

  • For the Easy Image service which is a part of CKEditor Cloud Services.

    You need to set up a Cloud Services account and once you created a token endpoint configure the editor to use it:

    ClassicEditor
        .create( editorElement, {
            cloudServices: {
                tokenUrl: 'https://example.com/cs-token-endpoint',
                uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/'
            }
        } )
        .then( ... )
        .catch( ... );
    

Disclaimer: These are proprietary services.

Custom upload adapter

You can also write your own upload adapter which will send files in the way you want to your server (or wherever you like to send them).

See Custom image upload adapter guide to learn how to implement it.

An example (i.e. with no security built-in) upload adapter can look like this:

class MyUploadAdapter {
    constructor( loader ) {
        // CKEditor 5's FileLoader instance.
        this.loader = loader;

        // URL where to send files.
        this.url = 'https://example.com/image/upload/path';
    }

    // Starts the upload process.
    upload() {
        return new Promise( ( resolve, reject ) => {
            this._initRequest();
            this._initListeners( resolve, reject );
            this._sendRequest();
        } );
    }

    // Aborts the upload process.
    abort() {
        if ( this.xhr ) {
            this.xhr.abort();
        }
    }

    // Example implementation using XMLHttpRequest.
    _initRequest() {
        const xhr = this.xhr = new XMLHttpRequest();

        xhr.open( 'POST', this.url, true );
        xhr.responseType = 'json';
    }

    // Initializes XMLHttpRequest listeners.
    _initListeners( resolve, reject ) {
        const xhr = this.xhr;
        const loader = this.loader;
        const genericErrorText = 'Couldn\'t upload file:' + ` ${ loader.file.name }.`;

        xhr.addEventListener( 'error', () => reject( genericErrorText ) );
        xhr.addEventListener( 'abort', () => reject() );
        xhr.addEventListener( 'load', () => {
            const response = xhr.response;

            if ( !response || response.error ) {
                return reject( response && response.error ? response.error.message : genericErrorText );
            }

            // If the upload is successful, resolve the upload promise with an object containing
            // at least the "default" URL, pointing to the image on the server.
            resolve( {
                default: response.url
            } );
        } );

        if ( xhr.upload ) {
            xhr.upload.addEventListener( 'progress', evt => {
                if ( evt.lengthComputable ) {
                    loader.uploadTotal = evt.total;
                    loader.uploaded = evt.loaded;
                }
            } );
        }
    }

    // Prepares the data and sends the request.
    _sendRequest() {
        const data = new FormData();

        data.append( 'upload', this.loader.file );

        this.xhr.send( data );
    }
}

Which can then be enabled like this:

function MyCustomUploadAdapterPlugin( editor ) {
    editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
        return new MyUploadAdapter( loader );
    };
}

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        extraPlugins: [ MyCustomUploadAdapterPlugin ],

        // ...
    } )
    .catch( error => {
        console.log( error );
    } );

NOTE: The above is just an example upload adapter. As such, it does not have security mechanisms built-in (such as CSRF protection).


I was searching for information on how to use this control and found the official documentation rather minimal. I did however get it to work after much trial and error, so I thought I would share.

In the end I used the CKEditor 5 simple upload adapter with Angular 8 and it works just fine. You do however need to create a custom build of ckeditor that has the upload adapter installed. It's pretty easy to do. I'am assuming you already have the ckeditor Angular files.

First, create a new angular project directory and call it "cKEditor-Custom-Build" or something. Don't run ng new (Angular CLI), but instead use npm to get the base build of the editor you want to show. For this example I am using the classic editor.

https://github.com/ckeditor/ckeditor5-build-classic

Go to to github and clone or download the project into your new shiny build directory.

if you are using VS code open the dir and open a terminal box and get the dependencies:

npm i

Right you now have the base build and you need to install an upload adapter. ckEditor has one. install this package to get the simple upload adapter:

npm install --save @ckeditor/ckeditor5-upload

..once this is done open the ckeditor.js file in the project. Its in the "src" directory. If you have been playing around with ckEditor then its contents should look familiar.

Import the new js file into the ckeditor.js file. There will be a whole load of imports in this file and drop it all the bottom.

import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter';

...Next add the import to your array of plugins. As I am using the classic editor my section is called "ClassicEditor.builtinPlugins", add it next to TableToolbar. That's it all configured. No additional toolbars or config needed at this end.

Build your ckeditor-custom-build.

npm run build

The magic of Angular will do its thing and a "build" directory is created in your project. That it for the custom build.

Now open your angular project and create a directory for your new build to live. I actually put mine in the assets sub-directory, but it can be anywhere you can reference it.

Create a directory within "src/assets" called something like "ngClassicEditor", it doesn't matter what you call it, and copy the build file into it (that you just created). Next in the component that you want to use the editor, add an import statement with the path to the new build.

import * as Editor from '@app/../src/assets/ngClassicEditor/build/ckeditor.js';

nearly done...

The final bit is to configure the Upload adapter with the API endpoint that the adapter should use to upload images. Create a config in your component class.

  public editorConfig = {
simpleUpload: {
  // The URL that the images are uploaded to.
  uploadUrl: environment.postSaveRteImage,

  // Headers sent along with the XMLHttpRequest to the upload server.
  headers: {
    'X-CSRF-TOKEN': 'CSFR-Token',
    Authorization: 'Bearer <JSON Web Token>'
  }
}

};

I'm actually using the environment transform here as the URI changes from dev to production but you could hardcode a straight URL in there if you want.

The final part is to configure your editor in the template to use your new configuration values. Open you component.html and modify your ckeditor editor tag.

     <ckeditor [editor]="Editor" id="editor"  [config]="editorConfig">
      </ckeditor>

That's it. You are done. test, test test.

My API is a .Net API and I am happy to share if you need some sample code. I really hope this helps.


It's working good for me. thanks for all answer's. this is my implementation.


myUploadAdapter.ts

import { environment } from "./../../../environments/environment";

export class MyUploadAdapter {
  public loader: any;
  public url: string;
  public xhr: XMLHttpRequest;
  public token: string;

  constructor(loader) {
    this.loader = loader;

    // change "environment.BASE_URL" key and API path
    this.url = `${environment.BASE_URL}/api/v1/upload/attachments`;

    // change "token" value with your token
    this.token = localStorage.getItem("token");
  }

  upload() {
    return new Promise(async (resolve, reject) => {
      this.loader.file.then((file) => {
        this._initRequest();
        this._initListeners(resolve, reject, file);
        this._sendRequest(file);
      });
    });
  }

  abort() {
    if (this.xhr) {
      this.xhr.abort();
    }
  }

  _initRequest() {
    const xhr = (this.xhr = new XMLHttpRequest());
    xhr.open("POST", this.url, true);

    // change "Authorization" header with your header
    xhr.setRequestHeader("Authorization", this.token);

    xhr.responseType = "json";
  }

  _initListeners(resolve, reject, file) {
    const xhr = this.xhr;
    const loader = this.loader;
    const genericErrorText = "Couldn't upload file:" + ` ${file.name}.`;

    xhr.addEventListener("error", () => reject(genericErrorText));
    xhr.addEventListener("abort", () => reject());

    xhr.addEventListener("load", () => {
      const response = xhr.response;

      if (!response || response.error) {
        return reject(
          response && response.error ? response.error.message : genericErrorText
        );
      }

      // change "response.data.fullPaths[0]" with image URL
      resolve({
        default: response.data.fullPaths[0],
      });
    });

    if (xhr.upload) {
      xhr.upload.addEventListener("progress", (evt) => {
        if (evt.lengthComputable) {
          loader.uploadTotal = evt.total;
          loader.uploaded = evt.loaded;
        }
      });
    }
  }

  _sendRequest(file) {
    const data = new FormData();

    // change "attachments" key
    data.append("attachments", file);

    this.xhr.send(data);
  }
}


component.html

<ckeditor
  (ready)="onReady($event)"
  [editor]="editor"
  [(ngModel)]="html"
></ckeditor>

component.ts

import { MyUploadAdapter } from "./myUploadAdapter";
import { Component, OnInit } from "@angular/core";
import * as DecoupledEditor from "@ckeditor/ckeditor5-build-decoupled-document";

@Component({
  selector: "xxx",
  templateUrl: "xxx.html",
})
export class XXX implements OnInit {
  public editor: DecoupledEditor;
  public html: string;

  constructor() {
    this.editor = DecoupledEditor;
    this.html = "";
  }

  public onReady(editor) {
    editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
      return new MyUploadAdapter(loader);
    };
    editor.ui
      .getEditableElement()
      .parentElement.insertBefore(
        editor.ui.view.toolbar.element,
        editor.ui.getEditableElement()
      );
  }

  public ngOnInit() {}
}

In React

Make a new file with MyCustomUploadAdapterPlugin

import Fetch from './Fetch'; //my common fetch function 

class MyUploadAdapter {
    constructor( loader ) {
        // The file loader instance to use during the upload.
        this.loader = loader;
    }

    // Starts the upload process.
    upload() {
        return this.loader.file
            .then( file => new Promise( ( resolve, reject ) => {

                const toBase64 = file => new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.readAsDataURL(file);
                    reader.onload = () => resolve(reader.result);
                    reader.onerror = error => reject(error);
                });
                
                return toBase64(file).then(cFile=>{
                    return  Fetch("admin/uploadimage", {
                        imageBinary: cFile
                    }).then((d) => {
                        if (d.status) {
                            this.loader.uploaded = true;
                            resolve( {
                                default: d.response.url
                            } );
                        } else {
                            reject(`Couldn't upload file: ${ file.name }.`)
                        }
                    });
                })
                
            } ) );
    }

   
}

// ...

export default function MyCustomUploadAdapterPlugin( editor ) {
    editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
        // Configure the URL to the upload script in your back-end here!
        return new MyUploadAdapter( loader );
    };
}

and in

import MyCustomUploadAdapterPlugin from '../common/ckImageUploader';
import CKEditor from '@ckeditor/ckeditor5-react';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';



  <CKEditor
         editor={ClassicEditor}
         data={quesText}
         placeholder="Question Text"
         config={{extraPlugins:[MyCustomUploadAdapterPlugin]}} //use
  />