# Configuring CKEditor in Apostrophe
Apostrophe uses CKEditor (opens new window) for rich text editing. It's a great rich text editor that seriously addresses the many cross-browser compatibility issues that come up due to the fact that each browser has its own unique implementation of the underlying rich text edit functionality.
You've seen how to add a rich text widget to a page and configure styles and toolbar controls, but sometimes you'll want to go beyond that and configure CKEditor for your project's specific needs. Here's how you can do that.
# Changing the allowed HTML tags in rich text
Some CKEditor configurations introduce new HTML tags that might be present in the markup. It looks great in the editor, but when you refresh the page they are gone. What happened?
Apostrophe automatically filters all rich text through the sanitize-html (opens new window) module. This prevents XSS attacks and also prevents the page design from being wrecked by bad or just plain ugly markup pasted from desktop applications.
However, sometimes the default configuration isn't working for you, for instance because you want to add sub
and sup
to the list of allowed tags.
It's easy to change: just set the sanitizeHtml
option in lib/modules/apostrophe-rich-text-widgets/index.js
in your project. Just keep in mind that if you configure one of the sanitizeHtml options at all, you must configure that option completely. A common mistake is forgetting to allow a
elements or the http
protocol, resulting in very pretty text with no links allowed!
Here are a few common configurations. For more, see the sanitize-html
documentation (opens new window).
# Add text styles using classes
You will often want to add styles to the rich text editor widget that use classes for visual styling. For example, you may have these configurations in an area with a rich text widget for a basic paragraph and two special styles:
{{ apos.area(data.page, 'body', {
widgets: {
'apostrophe-rich-text': {
toolbar: [ 'Styles', 'Bold', 'Italic', 'Link', 'Unlink' ],
styles: [
{ element: 'p', name: 'Paragraph' },
{
element: 'p',
name: 'Featured text',
attributes: { class: 'featured-text' }
},
{
element: 'h3',
name: 'Section title',
attributes: { class: 'section-heading' }
}
]
}
}
}) }}
You can allow these classes in two ways. First, you could allow all classes on those elements using allowedAttributes
:
// lib/modules/apostrophe-rich-text-widgets/index.js
module.exports = {
// The standard list copied from the module, plus sup and sub
sanitizeHtml: {
// allowedTags: [...],
allowedAttributes: {
'p': ['class'],
'h3': ['class'],
// Include the default setting as well, or else links will break
'a': [ 'href', 'name', 'target' ]
},
// ...,
}
};
TIP
You can open this up to allow the class
attribute on any element by replacing those individual tag name keys in allowedAttributes
with an asterisk string ('*': ['class']
).
Alternatively, you could only allow specific classes. You may not want to allow people to paste in rich text from somewhere else that includes classes that don't work well in a certain context. In this approach, you would use allowedClasses
:
// lib/modules/apostrophe-rich-text-widgets/index.js
module.exports = {
// The standard list copied from the module, plus sup and sub
sanitizeHtml: {
// allowedTags: [...],
allowedClasses: {
'p': [ 'featured-text' ],
'h3': [ 'section-heading' ]
},
// ...,
}
};
There are no default allowedClasses
settings, so you don't need to worry about including defaults for this one.
# Allow additional HTML tags
The default allowedTags
configuration from sanitize-html
is:
allowedTags: [
'h3', 'h4', 'h5', 'h6', 'blockquote',
'p', 'a', 'ul', 'ol', 'nl', 'li',
'b', 'i', 'strong', 'em', 'strike', 'abbr',
'code', 'hr', 'br', 'div', 'caption',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe'
],
To add tags to that list, you would include all of those tags you want to keep, then add the new ones. Here is an example adding sup
and sub
:
// lib/modules/apostrophe-rich-text-widgets/index.js
module.exports = {
// The standard list copied from the module, plus sup and sub
sanitizeHtml: {
allowedTags: [
'h3', 'h4', 'h5', 'h6', 'blockquote',
'p', 'a', 'ul', 'ol', 'nl', 'li',
'b', 'i', 'strong', 'em', 'strike', 'abbr',
'code', 'hr', 'br', 'div', 'caption',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe',
'sup', 'sub' // ⬅ The new tags
],
// ...,
}
};
There are many other combinations of similar configurations you may need to use. For more, see the sanitize-html
documentation (opens new window).
# Global CKeditor configuration
The apostrophe-areas module is responsible for initially loading CKEditor. On the browser side, the enableCkeditor method is responsible for setting global CKEditor properties like disableAutoinline
and calling CKEditor.plugins.addExternal
to add our split
plugin, which allows a toolbar control for splitting a rich text widget into two rich text widgets.
So extending that method at the project level is a sensible place to do more global configuration like this:
// in lib/modules/apostrophe-areas/public/js/user.js in your project folder
apos.define('apostrophe-areas', {
construct: function(self, options) {
// Use the super pattern - don't forget to call the original method
var superEnableCkeditor = self.enableCkeditor;
self.enableCkeditor = function() {
superEnableCkeditor();
// Now do as we please
CKEDITOR.plugins.addExternal('myplugin', '/modules/my-apostrophe-areas/js/ckeditorPlugins/YOUR-PLUGIN-NAME/', 'plugin.js');
};
}
});
What's going on in this code?
- In this example, we have placed our CKEditor plugin files in a directory at
lib/modules/apostrophe-areas/public/js/ckeditorPlugins/YOUR-PLUGIN-NAME
. That includes theplugin.js
file as well as any supporting files in their respective directories (e.g.,icons/pluginName.png
). - By placing the file in
lib/modules/apostrophe-areas/public/js/user.js
, we assure that it is pushed to the browser automatically. That module already pushesuser
as a script, and will push our project-level version too, providing a convenient place to extend a moog type. - Calling
apos.define('apostrophe-areas', { ... })
adds a new definition for the browser-side object that manages editable areas — basically, the browser's version of the areas module. When we do this, moog gives us an implicit subclass of the original type, replacing it with our enhanced version. - We then use the super pattern to extend the existing
enableCkeditor
method, calling the old version and then adding new functionality. - Inside that method, we call
CKEDITOR.plugins.addExternal
to add a CKEditor plugin (opens new window). Any toolbar buttons it makes available can now be used when configuring thetoolbar
option for the apostrophe-rich-text widget.- The primary plugin you are adding may have additional CKEditor plugin dependencies. You will add those with the same
CKEDITOR.plugins.addExternal
method.
- The primary plugin you are adding may have additional CKEditor plugin dependencies. You will add those with the same
- The URL of the plugin begins with
/modules/my-apostrophe-areas
. This path will always point to thepublic
subdirectory of your project-level extension of theapostrophe-areas
module (lib/modules/apostrophe-areas/public
in your project). Themy-
prefix is automatically added to distinguish it from the assets folder of the originalapostrophe-areas
module that ships with Apostrophe.
The CKEditor plugin then needs to be registered in the apostrophe-rich-text-widgets
widget as well:
// lib/modules/apostrophe-rich-text-widgets/public/js/editor.js
apos.define('apostrophe-rich-text-widgets-editor', {
construct: function(self, options) {
self.beforeCkeditorInline = function() {
// The 'myplugin' name should match what you added in the previous step.
// NOTE: Be sure to include `'split'` if you intend to use that feature.
// This will become unnecessary in a future release.
self.config.extraPlugins = ['myplugin', 'split'];
};
}
});
# Displaying icons for ckeditor plugins distributed with ckeditor
You will note that if the ckeditor plugin you are trying to add is one that is distributed by the official ckeditor team, but not one that is included in Apostrophe's custom ckeditor theme, you will not see a visible icon in the toolbar for that item.
If this happens you will likely discover that your plugin folder contains a styles
subdirectory with a suitable .css
file, but it is not being loaded.
You can address this in your copy of the plugin with the following code:
// in your plugin.js file
CKEDITOR.document.appendStyleSheet(pluginDirectory + 'styles/colorbutton.css');
# Instance-specific CKEditor configuration
It is also possible to do custom configuration at the time a rich text editor is fired up. That allows us to look at the options that were passed to that widget via apos.area
or apos.singleton
and decide what to do. It is also the best place do things like changing the list of buttons that CKEditor is currently disabling.
// in lib/modules/apostrophe-rich-text-widgets/public/js/editor.js in your project folder
apos.define('apostrophe-rich-text-widgets-editor', {
construct: function(self, options) {
self.beforeCkeditorInline = function() {
// Mess around with the `config` object about to be passed to CKEditor
self.config.removePlugins = 'man-i-hate-this-particular-plugin';
};
}
});
What's going on in this code?
- Placing the file in
lib/modules/apostrophe-rich-text-widgets/public/js/editor.js
ensures it is pushed to the browser. See the corresponding file innode_modules/apostrophe
. Again, if theeditor
asset is pushed by this module, our project-level version will get pushed too. No further configuration is needed on our part. - We implicitly subclass
apostrophe-rich-text-widgets-editor
. Apostrophe makes an instance of this type each time a rich text widget is ready to start editing. - We override the
beforeCkeditorInline
method, which is provided for our convenience. Inside this method we can modifyself.config
, which is the configuration object about to be passed to CKEditor.
If we want to, we can look at
self.options.templateOptions
, which contains the configuration passed to this widget byapos.areas
orapos.singleton
.
Apostrophe's initial definition of the
beforeCkeditorInline
method is empty (following the template pattern (opens new window)), but if you are using various add-on modules it's possible that some of them define it. If you want to be sure that code is called too, use the super pattern rather than just replacing the method outright.
# How to add the CKEditor font plugin
download font plugin version, specifically 4.14.0 (opens new window) for compatibility
unzip into the
lib/modules/apostrophe-areas/public/js/ckeditorPlugins/font
subdirectory of your project, never modify node_modules. This folder should already contain aplugin.js
and alang/
folder)create or edit
lib/modules/apostrophe-areas/public/js/user.js
as follow// in lib/modules/apostrophe-areas/public/js/user.js in your project folder apos.define('apostrophe-areas', { construct: function(self, options) { var superEnableCkeditor = self.enableCkeditor; self.enableCkeditor = function() { superEnableCkeditor(); // "my-" is important and makes sure the project-level folder is used CKEDITOR.plugins.addExternal('font', '/modules/my-apostrophe-areas/js/ckeditorPlugins/font/', 'plugin.js'); }; } });
add
style
toallowedAttributes
forspan
tags inlib/modules/apostrophe-rich-text-widgets/index.js
. Be aware this increases the potential for unexpected results when content is copied and pasted from other sitesNOTE
If the file does not exist, please use the snippet below to use the default values
// in lib/modules/apostrophe-rich-text-widgets/index.js in your project folder module.exports = { sanitizeHtml: { allowedAttributes: { a: [ 'href', 'name', 'target' ], img: [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ], span: [ 'style' ] } } };
enable the font plugin as an
extraPlugin
inlib/modules/apostrophe-rich-text-widgets/public/js/editor.js
// in lib/modules/apostrophe-rich-text-widgets/public/js/editor.js in your project folder apos.define('apostrophe-rich-text-widgets-editor', { construct: function(self, options) { self.beforeCkeditorInline = function() { // The "split" plugin is also present for Apostrophe's native // "split this rich text widget into two" functionality self.config.extraPlugins = ['font', 'split']; }; } });
add the
Font
andFontSize
tools in your rich-texttoolbar
option for your apostrophe-rich-text widget