How to enable image upload support in CKEditor 5?
Asked Answered
H

6

31

I will use the ckeditor v5 into my project. I´ve trying to use the image plugin, but I don´t find enough informations about it.

If you see the Demoe here, you easily upload images with Drag&Drop. But when I will try it with the download ballon zip nothing happens when I try to Drag&Drop a image. There is also no error.

Is there a way to use this image support in the downladable variant?

Hairpin answered 16/10, 2017 at 7:39 Comment(0)
B
47

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).

Beaudette answered 16/10, 2017 at 15:17 Comment(13)
so both are paid service? is there any free of charge way to do the uploads? like filebrowserUploadUrl in ckeditor4?Infection
As my answer says – you can also write you own upload adapter. There's even one 3rd party plugin which does just this (npmjs.com/package/ckeditor5-simple-upload).Beaudette
thx.. tested the simple-upload, yet it breaks for ckeditor5-beta1 at the moment.Infection
Thanks @Beaudette for the link, I finally can convert the ES6 syntax to generic browser-based JavaScript syntax here, just in case someone need it for making simple app.Accordingly
For passing CSRF tokens you can use xhr.setRequestHeader() in _initRequest() after xhr.open('POST', this.url, true);. for example for Laravel you should add xhr.setRequestHeader("X-CSRF-TOKEN", document.head.querySelector('meta[name="csrf-token"]').content);Nonconformity
seems loader.file.name shows undefined. I can't get the file name and extensions. could you help please?Appreciative
might be a stupid question, but HOW do you deal with this on server-side (e.g. with PHP?), I could not find ANY code and what seems to arrive at server-side is some "Promise" object without methodsDeery
Okay, figured it out. Your code sends a Promise to the HTTP server which does not make sense. I think, the issue is that this.loader is a promise itself. You need to wait for it to have finished and then you can use another promise (for receiving the server response) to send it to the server. Just use official example: ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/… Then, at PHP-side (no-one seems to care writing a simple example about this), request contains the upload or file object as a normal PHP file object and can be usedDeery
@Deery hi Sir, how did you do it?Lenee
@Lenee I used the official example from the link I posted, so the code from it... do you have any concrete question about his one?Deery
I am getting the error "Property 'uploader' does not exist on type 'CombinedVueInstance" what to doSalta
I have integrated custom uploader in NuxtJS but I am getting an error "Property 'uploader' does not exist on type 'CombinedVueInstance" on extraPlugins: [this.uploader]Salta
This is amazing, been searching all day to get this one resolved. Thanks a million @BeaudetteDiameter
B
16

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.

Breed answered 30/9, 2019 at 9:4 Comment(10)
I tried your example and followed all steps it is working and sending request to Server API but, request coming empty without image file I uploaded. What is reason for this ? It would be great if you know the reason ? My server is on Spring BootRecessive
I tried even custom hardcoded response like {"url": "image-url"} but it still giving errorRecessive
I want to save image to my resources folder and want return each image url, but images even not reaching to server. I am not sure SimpleUploadAdapter is adding images to request or not.Recessive
My server response returns as documentationRecessive
Yes I get that, but your API will return a HTTP response code to signify how the post went. To be honest I don't have any experience of Spring Boot, so you might want to post a question on how to debug incoming API POST operations.Breed
I got my error as you said it is related with debugging image was coming inside request I didn't debugged well. However, how should be the response format that I can confirm that image uploaded successfully. I am writing as documentation but I am getting alert box image not uploadedRecessive
I would finish if you help on this last comment. Anyway, you saved my dayRecessive
I am responding with random image on internet that might be reason that am I getting error ? { "url": "miro.medium.com/max/1200/1*mk1-6aYaf_Bes1E3Imhc0A.jpeg" }Recessive
Well the response really depends on how you have coded it. The HTTP specification says you should get a 201 Created response code and a copy of the newly created entity, however this depends on how the API is ultimately configured.Breed
Let us continue this discussion in chat.Breed
C
5

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() {}
}
Centra answered 17/6, 2020 at 8:2 Comment(0)
H
5

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
  />
Homespun answered 7/10, 2020 at 6:44 Comment(0)
D
2

for people experiencing issues with XHR, you can use fetch api as well and this seem to work fine

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

    request(file) {
      return fetch(this.url, { // Your POST endpoint
        method: 'POST',
        headers: {
          'x-csrf-token': _token
        },
        body: file // This is your file object
      });
    }

upload() {
        const formData = new FormData();

        this.loader.file.then((filenew) => {
          console.log(filenew);
          formData.append('file', filenew, filenew.name);
  
          return new Promise((resolve, reject) => {
            this.request(formData).then(
             response => response.json() // if the response is a JSON object
           ).then(
             success => console.log(success) // Handle the success response object
           ).catch(
             error => console.log(error) // Handle the error response object
           );
        })
      });
    }
Diameter answered 23/5, 2021 at 9:44 Comment(0)
F
0

I used this config:

public editorConfig = {
 simpleUpload: {
 uploadUrl: environment.postSaveRteImage,
 headers: {
'X-CSRF-TOKEN': 'CSFR-Token',
 Authorization: 'Bearer <JSON Web Token>'
 }
 }

images upload is successful and response is {"url": "image-url"}. but in front-end ckeditor's alert says

Cannot upload file: undefined.

Faeroese answered 25/2, 2021 at 9:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.