How to customize CKEditor with your own plugins, skins, configurations

This post summarizes what I've learned about customizing the open-source WYSIWYG rich-text editor CKEditor 3.5.2 with one's own plugins, skins, and configurations. There is already a lot of good resources so wherever possible I will link to them and just summarize and/or supplement them. However I've found no overall guide for customizing CKEditor and thus intend to fill this vacancy.

Introduction

CKEditor is a popular rich-text editor implemented in JavaScript. Its architecture makes heavy use of plugins and events and it is very configurable and customizable. Its documentation is thousandfold better than in the 2.x version but still you can't find there everything you might want.

To try out check the CKEditor's demo.

Customizing CKEditor

Layout of the customized CKEditor installation

The structure of our customized CKEditor is as follows:
  • <my web root>/myckeditor/
    • custom_configurations/
    • custom_plugins/
    • custom_skins/
    • editor/ - the original distribution of CKEditor, i.e. the content of ckeditor_3.5.2.zip/ckeditor/
      • _samples
      • _source/
      • lang/
      • ckeditor.js
      • ckeditor_source.js
      • ...

Custom configurations

You will need to change CKEditor's configuration and the best way to do so is perhaps to create your own configuration file, as described on the CKEditor's configuration page. You then tell CKEditor to load it when initializing it:


<textarea name="newedittextarea">initial content...</textarea>
<script type="text/javascript" src="/mywebapproot/myckeditor/editor/ckeditor.js"></script>
<script type="text/javascript">//<![CDATA[
CKEDITOR.replace('newedittextarea', { "customConfig": "/mywebapproot/myckeditor/custom_configs/my_config.js" , "RootPath":"/mywebapproot/"});
//]]></script>


I say "configurations" because we had two configurations (and two skins), a normal one and then a child version of the editor (less toolbars, bigger and friendlier icons, ...).

It's important to notice that the configuration file may contain any JavaScript code (incl. function declaration and use) and access variables and functions defined in the page prior to the inclusion of CKEditor and thus you can dynamically adjust the configuration based on the calling page. Possible use of this is shown in CKEditor: Hide some toolbar buttons on a per page basis.

You also need to be aware of the order of loading of the different configurations (in-page, custom file, in plugins) and ckeditor sources and its plugins (which differ between the production and development mode). So when something doesn't work as expected, make sure that you check the order in which things get applied and overridden.

Custom skins

This is how you create a custom skin based on the default kama skin:
  1. Copy myckeditor/editor/skins/kama/ (or ../editor/_source/skins/.. if you care more for readability than performance) to myckeditor/custom_skins/my_kama/
  2. Use a search & replace tool to replace all occurences of "kama" with "my_kama" in the copied files (you can do it automatically, no need to manually confirm each of the few hundred replacements)
    • Thus you'll have the CSS class cke_skin_my_kama instead of cke_skin_kama etc.
    • This renaming of all the classes is necessary for otherwise there will be conflicts between the default kama skin and the one of yours and things might not work (this applies at least when using the compressed production version of CKEditor, ckeditor.js)
    • It's important to get this step right otherwise CKEditor may fail to load
  3. Optional: Add a custom css to the skin:
    1. Create the CSS, e.g. at myckeditor/custom_skins/my_kama/my_icons/my_icons.css
    2. Register it with the skin: In my_kama/skin.js, add it to the appropriate component, in my case it's the editor:
      
      CKEDITOR.skins.add('my_kama',(function(){var a='cke_ui_color';return{
      editor:{css:['my_icons/my_icons.css','editor.css']},dialog:...
      
  4. Tell CKEditor to use the skin: In myckeditor/custom_configs/my_config.js, add:
    
    var ckeditorBasePath = CKEDITOR.basePath.substr(0, CKEDITOR.basePath.indexOf("editor/"));
    CKEDITOR.editorConfig = function( config ) {
       ...
       config.skin = 'my_kama,' + ckeditorBasePath + 'custom_skins/my_kama/';
       ...
    }
    
CKEditor supports multiple ways of specifying your custom skin and I found this one to be working for me. The property CKEDITOR.basePath used above is set automatically based on the include path of the ckeditor.js file.

Custom plugins

Creating custom plugins for CKEditor is a huge topic, we will touch only some parts of it. Refer to the plugin creation section in the Resources for introduction into plugin creation.

Dialog implementation options

Different types of dialog implementations
As far as I know you can create three types of dialogs with CKEditor 3.5:
  1. Native dialogs defined with JavaScript only - this is used by all CKEditor's own plugins, check them under _source/plugins
  2. IFrame dialogs - the dialog system defines the common elements such as title and Ok/Cancel buttons but the content of the dialog isn't defined by JS but it's an iframe loading another page (see dialog.addIframeDialog and the content type 'iframe'); requires the plugin iframedialog (an example below)
  3. Custom dialogs - they do not use the dialog API at all but define a custom command whose exec function displays a new window/jQuery dialog/whatever you want and pass results back using standard CKEditor methods - see [Eliasson10]
IFrame plugin example (showing only the dialog as there is nothing special in the plugin.js; config.RootPath is our custom config option set during the creation of the editor):


// myckeditor/custom_plugins/my_plugin/dialogs/my_plugin.js:
CKEDITOR.dialog.add( 'my_plugin', function( editor ) {
   return {
      title : 'My IFrame Plugin', minWidth : 390, minHeight : 230,
      contents : [ {
            id : 'tab1', label : '', title : '', expand : true, padding : 0,
            elements : [ {
                   type : 'iframe',
                   src : editor.config.RootPath + 'myCustomDialog.phtml',
                   width : 538, height : 478 - (CKEDITOR.env.ie ? 10 : 0)
            } ]
      } ]
      , buttons : []   // don't show the default buttons
   };
} );
Dialog's on OK handler options
The standard dialog API defines among others the events 'ok' and 'cancel' triggered when the corresponding dialog button is pressed (only available for native and iframe dialogs). You can (re)define their handler in two ways:
  1. In the dialog definition function via ", onOk: function(event) {... /* using this.getParentEditor()*/}"
  2. Outside of the dialog definition, for example inside a dialog iframe (option #2 above) by registering and deregistering the listener via CKEDITOR.dialog.getCurrent().on("ok", okListener) and CKEDITOR.dialog.getCurrent().removeListener("ok", okListener); (okListener being a function reference). To access the editor:
    
    var editor = window.parent.CKEDITOR.dialog.getCurrent()._.editor;
    

How to add a custom plugin

Registering the plugin with CKEditor & enabling it
In the custom CKEditor configuration file (myckeditor/custom_configs/my_config.js):
1. Register the plugin:

var ckeditorBasePath = CKEDITOR.basePath.substr(0, CKEDITOR.basePath.indexOf("editor/"));
var customPluginsRoot = ckeditorBasePath + 'custom_plugins/';

CKEDITOR.plugins.addExternal('my_plugin',customPluginsRoot+'my_plugin/', 'plugin.js'); CKEDITOR.plugins.addExternal('another_custom_plugin',customPluginsRoot+'another_custom_plugin/', 'plugin.js');

CKEDITOR.editorConfig = function( config ) { ...


Comments:
  • the first argument is the plugin name, as defined in its plugin.js (normally equal to its folder name)
  • the second argument is the path to the plugin folder
  • the third argument is always 'plugin.js'
There are also other ways to call CKEDITOR.plugins.addExternal (borrowed from the ResourceManager) but this one has worked for us.
2. Enable the plugin:

CKEDITOR.editorConfig = function( config ) {
   ...
   config.extraPlugins = 'my_plugin,another_custom_plugin';
  ...
}


Comments:
  • just specify the name of the (registered) plugin in a comma-separated string
3. Add it to the toolbar:

CKEDITOR.editorConfig = function( config ) {
   ...
    config.toolbar_Default = [
        ['Preview', 'Source','Link'],['MyPlugin', 'AnotherCustomPlugin']
    ];
   ...
}


Comments:
  • The name, 'MyPlugin', is the CKEDITOR.ui.button name specified as the first parameter to editor.ui.addButton in plugin.js. It is not the plugin name (my_plugin) - see below.
Plugin structure and naming
Files:
  • custom_plugins/plugin_name/
    • plugin.js
    • dialogs/plugin_name.js
Sample plugin.js for the my_plugin plugin:


CKEDITOR.plugins.add( 'my_plugin',
{
   requires : [ 'iframedialog' ],
   init : function( editor )
   {
      var command = editor.addCommand( 'my_plugin', new CKEDITOR.dialogCommand( 'my_plugin' ) );

editor.ui.addButton( 'MyPlugin', { label : 'ids:My label...', command : 'my_plugin' });

CKEDITOR.dialog.add( 'my_plugin', this.path + 'dialogs/my_plugin.js' ); } });


Comments:
  • the names of the plugin, of its command, and of its dialog are all my_plugin
  • the name of the plugin's toolbar button is MyPlugin - this may be added to config.toolbar_{name} in the ckeditor configuration
Sample iframe plugin dialog:


// myckeditor/custom_plugins/my_plugin/dialogs/my_plugin.js:
CKEDITOR.dialog.add( 'my_plugin', function( editor ) {
   return {
      title : 'My IFrame Plugin', minWidth : 390, minHeight : 230,
      contents : [ {
            id : 'tab1', label : '', title : '', expand : true, padding : 0,
            elements : [ {
                   type : 'iframe',
                   src : editor.config.RootPath + 'myCustomDialog.phtml',
                   width : 538, height : 478 - (CKEDITOR.env.ie ? 10 : 0)
            } ]
      } ]
      , buttons : []   // don't show the default buttons
   };
} );


Comments:
  • Nothing special here except of the config option editor.config.RootPath which we set ourselves when instantiating the ckeditor
Plugin toolbar icon
There are two ways to assign an icon to your plugin's button: either use the property icon when registering it, pointing to an image, or use the CSS class assigned to the "button".
Using the icon property:
(Copied from c_schmitz' comment on Voofie's plugin guide):


CKEDITOR.plugins.add('myplugin',
{
    init: function(editor)
    {
        editor.ui.addButton('myplugin',
            {
                label: 'myplugin',
                command: myplugin,
                icon: CKEDITOR.plugins.getPath('myplugin') + 'myplugin.png'
            });
    }
});
Using the CSS class:
CKEditor automatically assigns the CSS class "cke_button_" to toolbar elements representing plugin buttons, the command name being usually the same as the plugin name (e.g. "cke_button_my_plugin" for the plugin above) - though you can also specify your own class via the className property.

You then just need to define the class so that it contains a background image, likely from a sprite - see kama/icons.png and kama/editor.css.

Example custom css in myckeditor/custom_skins/my_kama/my_icons/my_icons.css using the sprite my_kama/my_icons/my_icons.png and registered with the custom skin as described above:


/* Plugins re-using native CKEditor's icons (the default) */
.cke_button_my_plugin .cke_icon {
    background-position: 0 -528px !important;
}

/* Plugins using our own icon sprite */ .cke_button_another_custom_plugin .cke_icon { background: url("my_icons.png") no-repeat scroll 0 -1232px transparent !important; }


Comments:
  • ckeditor's own icon sprite is loaded as the background image by default, you need to use !important to override it
  • the custom sprite image url is relative to the location of the css file
  • to add this custom CSS to the skin you need to add it its skin.js as described above

Custom language

CKEditor supports quite a number of languages out of the box and can auto-detect which one to use based on the users' browser settings. You can add a language of your own and tell CKEditor explicitly to use it:
  1. Create myckeditor/editor/lang/mylanguage.js based e.g. on editor/_source/lang/en.js
    • It should include all the keys and certainly it must include all the sub-groups (such as contextmenu: {...}) to avoid errors because of undefined properties.
  2. Tell CKEditor that the language is enabled and that it should be used by default:
    
    // myckeditor/custom_configs/my_config.js:
    CKEDITOR.editorConfig = function( config ) {
       ...
       CKEDITOR.lang.languages.mylanguage = 1;
       config.language='mylanguage';
       ...
    }
    
(CKEditor will automatically look for mylanguage.js under its lang/ folder.)

We have actually used our own internationalization system to provide the translation suitable for the current user. It is generated dynamically by a file included before ckeditor.js and producing a JavaScript object in the form expected by CKEditor:


// myckeditor/generate_mylanguage.js.php:
// ... PHP code here ... - produces JavaScript code similar to this:
// window.GENERATED_LANG = {dir:'ltr', editorTitle:'My Editor', ...};


The language file mylanguage.js only copies the JavaScript object with translations into the place expected by CKEditor:


// myckeditor/editor/lang/mylanguage.js:
CKEDITOR.lang['mylanguage'] = GENERATED_LANG;

Note on the CKEditor's modes: production vs. development

In the production you normally use a compressed version of CKEditor, which contains both the editor and its default plugins in one compressed file, ckeditor.js. On the other hand, during development, you use for the sake of easier debugging and troubleshooting the source version ckeditor_source.js, which reads the files under the _source/ folder. It's important to know that the two versions do not behave exactly the same and thus you need to test with both.

The main difference I've noticed with CKEditor 3.5.2 is that in the development mode plugins are loaded after the custom configuration file and thus you won't be able to override some of their settings, such as CKEDITOR.config.smiley_descriptions. In the production mode the plugins are loaded together with the editor itself before the custom configuration and thus everything will behave as expected.

If using both modes, you will need to synchronize same files, such as editor/lang/ with editor/_source/lang/ etc.

Resources

Warning: Some of the resources may be out-of-date. Always check their publication date and the current CKEditor APIs and docs.

CKEditor in general

Building plugins

Overall tutorials etc.

Specialized plugin topics

IFrame dialogs

  • stackoverflow: Example of an iframe dialog with custom onOk listener - setting a custom ok listener (to the CKEditor-provided OK button) is perhaps the best way to pass values from an iframe dialog back to the editor and it's also the standard way
  • Forum: Working with iframe dialog plugins - it's little chaotic and not all information there is true or the best solution possible but still it may be useful regarding how to react to the OK/Cancel buttons and how to pass data from the dialog back to the editor; generally the "Tutorial: create external plugin for CKEDITOR" referenced above is a more reliable source

Very special general/plugin topics that may be useful at some point

Additional unsorted links of interest

My other related blog posts

Post scriptum

I'd like to thank my employer, Iterate AS, for supporting me in writing this blog post and our client for giving me the opportunity to learn all this stuff.

Tags: library


Copyright © 2024 Jakub Holý
Powered by Cryogen
Theme by KingMob