Creating a Free Custom Build of CKEditor 5 for your React app.

October 24, 2022
11 min read

CKEditor is a super popular Wysiwyg editor.  It has a ton of built in plugins, and it's open source at it's core, which is amazing.  They also have several paid plugins, which are great, but if you're not a sizable company, it doesn't make a lot of sense to have the monthly burden of their paid premium plugins.  The default builds, however, have these pro plugins built into the toolbars, which I don't want.  For the more recent websites I've made, I have needed to make the following changes:

1. Create a simple image uploader.
2. Change the Wysiwyg editor to default to Markdown mode.
3. Add the Codeblock plugin to the toolbar.

Getting Started

Whatever your customizations are going to be, you need to start by download the source.  You can do that with git:

git clone https://github.com/ckeditor/ckeditor5.git

Copy

Once you have it downloaded, open it up in VS Code or your editor of choice.

Open up a terminal and navigate to the ckeditor5-build-classic folder

cd packages\ckeditor5-build-classic

Copy

then run the following to download all the necessary external packages:

npm i

Copy

CREATING THE NEW BUILD

There are several default builds already in the file system, I chose the "classic" one since that's closest to my needs.  Feel free to pick another one if that if closer to what you need.

Open the src/ckeditor.js file.  This is the file that handles the build.  You can see the original file here: https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-build-classic/src/ckeditor.js

PACKAGE UPDATES

Configuring a Simple Image Uploader

They provide an open source simple image uploader, though it's not in not in the default build so we need to add that.

At the top in the "imports" section, add the following:

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

Copy

Also, remove the following, which was the default:

import UploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';

Copy

Then, in the section where you define which plugins to add to the ClassicEditor, remove the following:
UploadAdapter
CKBox
CKFinder

and add:
SimpleUploadAdapter

Configuring Markdown mode

Add the following import:

import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';

Copy

Then, add a function to setup the processor and so that we'll be able to access this processor when we implement:

function Markdown(editor) {
    editor.data.processor = new GFMDataProcessor(editor.editing.view.document);
}

Copy

Add "Markdown" to the builtinPlugins array.

 

Add a CodeBlock to the toolbar

I am a coder and some of the sites I create are technical in nature, so having the ability to add code is a necessity.

Add the following import:

import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';

Copy

Then add "CodeBlock" to the builtinPlugins array.

At this point, your ckeditor.js file will look similar to the following:



// The editor creator to use.
import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';

import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
// import UploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter';
import Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
// import CKBox from '@ckeditor/ckeditor5-ckbox/src/ckbox';
// import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
import EasyImage from '@ckeditor/ckeditor5-easy-image/src/easyimage';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
import ImageStyle from '@ckeditor/ckeditor5-image/src/imagestyle';
import ImageToolbar from '@ckeditor/ckeditor5-image/src/imagetoolbar';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
import Indent from '@ckeditor/ckeditor5-indent/src/indent';
import Link from '@ckeditor/ckeditor5-link/src/link';
import List from '@ckeditor/ckeditor5-list/src/list';
import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import PasteFromOffice from '@ckeditor/ckeditor5-paste-from-office/src/pastefromoffice';
import PictureEditing from '@ckeditor/ckeditor5-image/src/pictureediting';
import Table from '@ckeditor/ckeditor5-table/src/table';
import TableToolbar from '@ckeditor/ckeditor5-table/src/tabletoolbar';
import TextTransformation from '@ckeditor/ckeditor5-typing/src/texttransformation';
import CloudServices from '@ckeditor/ckeditor5-cloud-services/src/cloudservices';

// custom stuff
import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter';
import GFMDataProcessor from '@ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor';
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';

function Markdown(editor) {
	editor.data.processor = new GFMDataProcessor(editor.editing.view.document);
}

export default class ClassicEditor extends ClassicEditorBase {}

// Plugins to include in the build.
ClassicEditor.builtinPlugins = [
	Essentials,
	// UploadAdapter,
	Autoformat,
	Bold,
	Italic,
	BlockQuote,
	// CKBox,
	// CKFinder,
	CloudServices,
	EasyImage,
	Heading,
	Image,
	ImageCaption,
	ImageStyle,
	ImageToolbar,
	ImageUpload,
	Indent,
	Link,
	List,
	MediaEmbed,
	Paragraph,
	PasteFromOffice,
	PictureEditing,
	Table,
	TableToolbar,
	TextTransformation,

	// custom
	SimpleUploadAdapter,
	Markdown,
	CodeBlock,
];

// Editor configuration.
ClassicEditor.defaultConfig = {
	toolbar: {
		items: [
			'heading',
			'|',
			'bold',
			'italic',
			'link',
			'bulletedList',
			'numberedList',
			'|',
			'outdent',
			'indent',
			'|',
			'uploadImage',
			'blockQuote',
			'insertTable',
			'codeBlock',
			'mediaEmbed',
			'undo',
			'redo',
		],
	},
	image: {
		toolbar: [
			'imageStyle:inline',
			'imageStyle:block',
			'imageStyle:side',
			'|',
			'toggleImageCaption',
			'imageTextAlternative',
		],
	},
	table: {
		contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells'],
	},
	// This value must be kept in sync with the language defined in webpack.config.js.
	language: 'en',
};

Copy

Go ahead and build the package:

npm run-script build

Copy

Congratulations, you now have a custom build.  You have completed half the battle.  Now we just need to implement it in a solution.

IMPLEMENTATION OF YOUR CUSTOM BUILD

In your target React project, create a new folder at the same level as your "src" folder.  I called mine "vendor", then I added another folder called "@ckeditor\ckeditor5-build-classic-simple-upload".  Inside this folder, copy over the following files from your build folder from above:

1. the “build” folder (and everything within it)
2. changelog.md
3. license.md
4. package.json
5. readme.md

Having this package local is an easy way to use it.  You could also add the new package to npm and use it via normal npm/yarn methods. I'm just showing you a shortcut.

In your package.json file in your target app, add the following dependency:
"ckeditor5-build-classic-simple-upload": "file:./vendor/@ckeditor/ckeditor5-build-classic-simple-upload",

Copy

You'll also need to add the following packages:

npm i @ckeditor/ckeditor5-react
npm i @ckeditor/ckeditor5-upload

Copy

LET'S USE IT IN A COMPONENT

I'm going to let the code speak for itself, but this is an example of a component that you can create with your new custom build:

import React, { Component } from 'react';
import { CKEditor } from '@ckeditor/ckeditor5-react';
// this is built and then placed into the root/vendor/ folder
import MarkdownEditor from 'ckeditor5-build-classic-simple-upload'; 
import SystemContext from 'context/SystemContext';



class Wysiwyg extends Component {
    static contextType = SystemContext;

    render() {
    
    	// you might need to customize headers in you have to have a bearer token or other methods

        const headers = {
            'X-CSRF-TOKEN': 'CSFR-Token',
            'Authorization': 'Bearer ' + this.context.getToken(),
        }
        
        return ( 
            <div className="App">                
                <CKEditor
                    editor={ MarkdownEditor }
                    config={{
                        simpleUpload: {
                        	// you will have to create an endpoint to catch the file POST
                            uploadUrl: `${process.env.REACT_APP_WEBAPI_URL}/api/media/image`,
                            headers: headers
                        },                        
                        codeBlock: {
                            languages: [
                                { language: 'plaintext', label: 'Plain text' }, 
                                { language: 'cs', label: 'C#' },
                                { language: 'css', label: 'CSS' },
                                { language: 'html', label: 'HTML' },
                                { language: 'javascript', label: 'JavaScript' },
                                { language: 'php', label: 'PHP' },
                                { language: 'typescript', label: 'TypeScript' },
                                { language: 'xml', label: 'XML' }
                            ]
                        }
                    }}
                    data={this.props.value || ""}
                    onReady={ editor => {
                        // You can store the "editor" and use when it is needed.
                        // console.log(Array.from( editor.ui.componentFactory.names() ));                        
                    } }
                    onChange={ ( event, editor ) => {
                        const data = editor.getData();
                        // console.log( { event, editor, data } );
                        
                        if(this.props.onChange) {
                            // console.log(data);
                            this.props.onChange(data, this.props.name);
                        }                                               
                    } }
                    onBlur={ ( event, editor ) => {
                        // console.log( 'Blur.', editor );
                    } }
                    onFocus={ ( event, editor ) => {
                        // console.log( 'Focus.', editor );
                    } }
                />
            </div>
        );
    }
}

export default Wysiwyg;

Copy

To summarize, CKEditor is amazing and it is my default choice for a Wysiwyg editor.  It's take a bit of understand to figure out how to handle a custom build, but hopefully this post will help ease the struggle.

Check out their docs for more help customizing your build.