Jul 25, 2017

How to use CKEditor 4 with Webpack

Webpack is my bundler of choice, and although mostly every package I use is compatible with it out of the box, there is one, very necessary one by the way, which is not: CKEditor 4.

The problem is that CKEditor loads extra files it needs from a path on the server, and we need to make those files available using Webpack.

But, fear not, because webpack is flexible enough so that we can make it work.

The tools I use, and what versions (at the time of writing):

  • Yarn (npm should work too) - 0.27.5
  • Webpack - 3.3.0
  • Webpack File Loader - 0.11.2
  • CKEditor - 4.7.1

What is not covered in this guide (which is required)

Steps to make it work:

  1. # Add CKEditor to you app.
  2. # Make webpack's publicpath available as an environmental variable.
  3. # Create a new module to setup and load CKEditor.
  4. # Import you module in to your entrypoint.
  5. # Optional: Reduce webpack compilation time.

Add CKEditor to you app

Using your terminal include CKEditor to your app:

cd /path/to/your_app/root/
yarn add "ckeditor@^4.x"

That should install the latest CKEditor form the 4.x branch. CKEditor 5 should be compatible with Webpack out of the box.

Make webpack's publicPath available as an environmental variable

When loading CKEditor, you give it the path from where it should load the extra files it needs. This is done with the global CKEDITOR_BASEPATH variable. The path we need is the one that Webpack will be serving the files from, which in Webpack is set using the output.publicPath option.

To have it available to your module, it needs to be on an environmental variable, which at the time of compiling the files will be replaced by Webpack using the EnvironmentPlugin.

Open your webpack configuration file (where you are defining output.publicPath) and add the following line:

/* your webpack configuration.js file */

// Set webpack public_path as an ENV variable.
process.env.WEBPACK_PUBLIC_PATH = variableContaining_output.publicPath

Create a new module to setup and load CKEditor

The module you are going to create does 4 things:

  1. Setup the basic CKEditor configuration options including the path form where CKEditor is to load its files.
  2. Tell Webpack to include make available all the CKEditor files that it will then load form the browser.
  3. Include the main ckeditor.js file on to your Webpack bundle.
  4. Initialize CKEditor (to create the editors on your page).

First, create a new module named ckeditor_loader, and make sure it is somewhere where Webpack can find it. Then create the files index.jsconfig.jsloader.js, styles.js and optionally contents.css (if you are going to use the iframe version of CKEditor).

cd /app/webpack/modules/path/
mkdir ckeditor_loader
cd ckeditor_loader
touch index.js loader.js config.js styles.js contents.css

Now add the content to each file:

Your module's entry file: Index.js

This is the main entry point to you module, it loads the rest of your files, and initializes the editors on your page.

/* index.js */

import './loader.js'
import 'ckeditor/ckeditor'

// You can replace this with you own init script, e.g.:
// - jQuery(document).ready()
window.onload = function () {
  window.CKEDITOR.replaceAll()
}

With this file, all <textarea> tags should now have a CKEditor instance with them.

The file where we make CKEditor compatible with Webpack: loader.js

This file will make sure all the files that CKEditor needs, are available via Webpack. It can (and should) be modified to suit your own deployment needs. I explain how to do it later on.

/* loader.js */

window.CKEDITOR_BASEPATH = `${process.env.WEBPACK_PUBLIC_PATH}node_modules/ckeditor/`

// Load your custom config.js file for CKEditor.
require(`!file-loader?context=${__dirname}&outputPath=node_modules/ckeditor/&name=[path][name].[ext]!./config.js`)

// Load your custom contents.css file in case you use iframe editor.
require(`!file-loader?context=${__dirname}&outputPath=node_modules/ckeditor/&name=[path][name].[ext]!./contents.css`)

// Load your custom styles.js file for CKEditor.
require(`!file-loader?context=${__dirname}&outputPath=node_modules/ckeditor/&name=[path][name].[ext]!./styles.js`)

// Load files from plugins.
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/plugins/',
  true,
  /.*/
)

// Load CKEditor lang files.
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/lang',
  true,
  /.*/
)

// Load skin.
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/skins/moono-lisa',
  true,
  /.*/
)

Your CKEditor configuration files: config.js, styles.js and content.js

These files come with CKEditor originally, and you should modify them to suit your needs. You can find the originals on the CKEditor folder. Copy the contents of each of those files to yours, and edit them.

If you want to start with a vanilla CKEditor, use the following files:

/* styles.js */

// This file contains style definitions that can be used by CKEditor plugins.
//
// The most common use for it is the "stylescombo" plugin which shows the Styles drop-down
// list containing all styles in the editor toolbar. Other plugins, like
// the "div" plugin, use a subset of the styles for their features.
//
// If you do not have plugins that depend on this file in your editor build, you can simply
// ignore it. Otherwise it is strongly recommended to customize this file to match your
// website requirements and design properly.
//
// For more information refer to: http://docs.ckeditor.com/#!/guide/dev_styles-section-style-rules

window.CKEDITOR.stylesSet.add('default', [])
/* config.js */

window.CKEDITOR.editorConfig = function (config) {
  // Define changes to default configuration here.
  // For complete reference see:
  // http://docs.ckeditor.com/#!/api/CKEDITOR.config
}

And use an empty content.css file.

Import you module in to your entrypoint

Your module is now ready to be used, and all that is left is to import it on your entry point, so open you main js file for your app and include the following line:

/* application.js /*

// Include somewhere in your file:
import 'ckeditor_loader'

Optional: Reduce webpack compilation time

By now, CKEditor should be loading on your site, but compilation time, specially when using webpack-dev-server, is less than ideal. The reason for this is that Webpack is compiling all the files from CKEditors plugins and languages folders, which is a waste of time and resources.

The solution is to limit the compiled files to only those from a plugin and a language that you actually need, and is done by changing your loader.js files to something like this:

/* loader.js */

window.CKEDITOR_BASEPATH = `${process.env.WEBPACK_PUBLIC_PATH}node_modules/ckeditor/`

// Load your custom config.js file for CKEditor.
require(`!file-loader?context=${__dirname}&outputPath=node_modules/ckeditor/&name=[path][name].[ext]!./config.js`)

// Load your custom contents.css file in case you use iframe editor.
require(`!file-loader?context=${__dirname}&outputPath=node_modules/ckeditor/&name=[path][name].[ext]!./contents.css`)

// Load your custom styles.js file for CKEditor.
require(`!file-loader?context=${__dirname}&outputPath=node_modules/ckeditor/&name=[path][name].[ext]!./styles.js`)

// Load files from plugins, excluding lang files.
// Limit to active plugins with
// Object.keys(CKEDITOR.plugins.registered).sort().toString().replace(/,/g, '|')
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/plugins/',
  true,
  /^\.\/((plugins|needed|by|ckeditor)(\/(?!lang\/)[^/]+)*)?[^/]*$/
)

// Load lang files from plugins.
// Limit to active plugins with
// Object.keys(CKEDITOR.plugins.registered).sort().toString().replace(/,/g, '|')
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/plugins/',
  true,
  /^\.\/(plugins|needed|by|ckeditor)\/(.*\/)*lang\/(en|es)\.js$/
)

// Load CKEditor lang files.
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/lang',
  true,
  /(en|es)\.js/
)

// Load skin.
require.context(
  '!file-loader?name=[path][name].[ext]!ckeditor/skins/moono-lisa',
  true,
  /.*/
)

There are 3 differences with the previous loader.js file you had:

  1. Regex for plugins is different, only loads plugins you use, and no language files.
  2. Regex for language is different, only loads languages you use.
  3. Plugins are now split, and plugins' language files have their own call to require.context().

 

What you need to know is which languages you want CKEditor to be available in to you users, and which plugins you are using.

Languages is up to you, but figuring out which plugins you are using is not. To do so, first configure CKEditor to exactly what you need, and once you are happy with the editor you have, open a browser to a page where you are loading an editor instance, and run the following in your browser's console:

Object.keys(CKEDITOR.plugins.registered).sort().toString().replace(/,/g, '|')

That line should give you a list of all the plugins that you are using. Mine looks something like this:

"a11yhelp|about|basicstyles|blockquote|button|clipboard|codeTag|codesnippet|contextmenu|copyformatting|dialog|dialogadvtab|dialogui|divarea|elementspath|enterkey|entities|fakeobjects|filebrowser|find|floatingspace|floatpanel|format|horizontalrule|htmlwriter|image|image2|indent|indentlist|justify|lineutils|link|list|listblock|magicline|maximize|menu|menubutton|notification|panel|pastefromword|pastetext|popup|prismhighlighter|removeformat|resize|richcombo|scayt|showblocks|showborders|sourcearea|specialchar|stylescombo|tab|table|tableselection|tabletools|toolbar|undo|widget|widgetselection|wsc|wysiwygarea" = $1

What you need is what's between the cuote marks. So now:

  • replace "plugins|needed|by|ckeditor" with the output from above without quote marks (needs to be replaced in two places), and
  • replace "en|es" with the list of languages you want to load separated by a pipe character, or if you only need english, just use "en" without the pipe character (this also two times).

Now your compilation time should be much lower, mine went from 33 to 11 seconds.


How do you integrate CKEditor with Webpack? Please let me know if you have a different method.

Comments