CKEditor 4 Custom Plugin Development Tips for PRO

By | February 21, 2017

I am developing a complex CKEditor plugin for typing in 23 Indian languages based on my PramukhIME JavaScript Library. If you are developing a complex plugin, CKEditor documentation is of little help. Here are some tips on what to do when you need certain features

About PramukhIME JavaScript Library

PramukhIME JavaScript Library is a highly customizable JavaScript library which supports typing in 23 Indian languages. It consists of two parts.

  1. IME library which deals with browser
  2. Keyboard plugin which processes user input and outputs Indian language characters

CKEditor Plugin Capability

I need following capabilities for my CKEditor plugin

  • Dynamically load IME library, Keyboard library
  • Dynamically load IME specific translation, keyboard specific translation
  • Dynamically load keyboard specific css file
  • Create a drop down list with list of languages
  • Change drop down list title to add/remove the shortcut key
  • Open dialog box to show language specific settings
  • Open dialog box to show language specific quick and detailed help
  • Change button icon based on the language selected from the drop down

Development Environment

CKEditor 4.7.1 (Source code)
CKEditor_ROOT refers to the location of CKEditor source

Prerequisites/Precautions

It is assumed that you are very well aware of creating a simple CKEditor plugin and know what type of code will go where. The code given below is given in chunk to get you started. List of tips provided are not in the chronological order of plugin development. The code is not incremental and it may omit the code that was written in any other step.

List of available tips

Here is a list of available tips in this tutorial so you can quickly look at it without scrolling through the page.

  • Explicitly list supported translation languages
  • Add dependency on other plugin
  • Access CKEditor Configuration options (aka plugin parameter)
  • Add plugin translation from multiple files
  • Add command to perform an action
  • Make command (and not button) available in wysiwyg and source mode
  • Add button to execute a command
  • Enable/Disable command/button based on a condition
  • Save reference of a button to perform action later on (like changing icon)
  • Dynamically change button icon
  • Add a button to open a dialog
  • Add a dropdown/Combo box
  • Dynamically change dialog title
  • Create a tabbed dialog
  • Dynamically load JavaScript and css files
  • Get underlying editable element

Explicitly list supported translation languages

CKEDITOR.plugins.add('pramukhime',
{
	lang: ['en'], // mention all your supported languages here
	init: function (editor) {
		// code comes here
	}
});

Add dependency on other plugin

CKEDITOR.plugins.add('pramukhime',
{
	requires: ['richcombo'], // plugin is dependent on richcombo plugin
	lang: ['en'], // mention all your supported languages here
	init: function (editor) {
		// code comes here
	}
});

Access CKEditor Configuration options (aka plugin parameter)

If you look at the sample plugins available in CKEditor source code, you would notice that the plugin options are defined after the plugin definition ends. So if I use it that way, here is an example

CKEDITOR.plugins.add('pramukhime',
{
	lang: ['en'], // mention all your supported languages here
	init: function (editor) {
		console.log(editor.config.pramukhime_options.selected_value); // will show "pramukhindic:sanskrit"
	}
});
// Here pramukhime_options is defined which can be used in the "init" function to customize the plugin
CKEDITOR.config.pramukhime_options = {
				languages: [],
				selected_value: 'pramukhindic:sanskrit',
				toggle_key: {key:120, ctrl: false, alt:false, title: 'F9'}
			};

The issue with the above approach is, developer needs to provide all the properties of option values. So the option properties become “all or none”. I have used following approach so that developer can provided only those properties which they want to override, keeping rest of the properties with default values.

CKEDITOR.plugins.add('pramukhime',
{
	lang: ['en'], // mention all your supported languages here
	init: function (editor) {
		var self = this;
		self.options = CKEDITOR.tools.extend (
			{
				languages: [],
				selected_value: 'pramukhindic:sanskrit',
				toggle_key: {key:120, ctrl: false, alt:false, title: 'F9'}
			}
			, editor.config.pramukhime_options, true); // the last argument "true" overwrites the property when extending object
		// If the developer provides value for pramukhime_options in the main configuration, it will use only those properties and rest of the values will be the default
		console.log(editor.config.pramukhime_options.selected_value); // will show either developer provided value or "pramukhindic:sanskrit" 
	}
});

Add plugin translation from multiple files

My plugin needs multiple translation files. One for main IME JavaScript and one translation file per each keyboard. The current CKEditor has a limitation that once the translation is added for a given plugin, if we want to add additional translation, we cannot use “CKEDITOR.plugins.setLang” function as it will overwrite the earlier object. So I had to “extend” the core CKEditor functionality to allow adding translation to an existing plugin translation object

// CKEditor_ROOT/plugins/pramukhime/plugin.js file
CKEDITOR.plugins.add('pramukhime',
{
	lang: ['en'], // mention all your supported languages here
	init: function (editor) {
		// Dynamically load CKEditor_ROOT/pramukhime/langs/en_pramukhindic.js file which calls CKEDITOR.plugins.addLang function
	}
});
// following function is a modified version of CKEDITOR.plugins.setLang
CKEDITOR.plugins.addLang = function (pluginName, languageCode, languageEntries) {
    var plugin = this.get(pluginName),
		pluginLangEntries = plugin.langEntries || (plugin.langEntries = {}),
		pluginLang = plugin.lang || (plugin.lang = []);

    if (CKEDITOR.tools.indexOf(pluginLang, languageCode) == -1)
        pluginLang.push(languageCode);
    pluginLangEntries[languageCode] = CKEDITOR.tools.extend(pluginLangEntries[languageCode], languageEntries);
};
// CKEditor_ROOT/plugins/pramukhime/langs/en_pramukhindic.js
CKEDITOR.plugins.addLang('pramukhime', 'en',
{
	pramukhindic:
	{
		pramukhindic: 'Pramukh Indic'
	}
});

Add command to perform an action

// in the "init" function.
editor.addCommand('PramukhIMEToggleLanguage', {
	exec: function (editor) {
		// perform an action
		editor.pramukhime.toggleLanguage();
	}
});

Make command (and not button) available in wysiwyg and source mode

In CKEditor, button executes command and the command may or may not be available in wysiwyg and source mode. By default, when we add a command using “editor.addCommand”, the command (and hence button is enabled) is available to wysiwyg mode only. If you want it to be available for specific modes, here is the code.

// in the "init" function.
editor.addCommand('PramukhIMEToggleLanguage', {
	exec: function (editor) {
		// perform an action
		editor.pramukhime.toggleLanguage();
	},
	modes: { wysiwyg: 1, source: 1}
});

Add button to execute a command

// in the "init" function. Before this code, command PramukhIMEToggleLanguage should have been added 
editor.ui.addButton('PramukhIMEToggleLanguage',
	{
		label: editor.lang.pramukhime.plugin_title,
		command: 'PramukhIMEToggleLanguage',
		toolbar: 'pramukhime' // specify toolbar name
	});

Enable/Disable command/button based on a condition

As mentioned earlier, button executes command. Even though you can disable the button, you should not. If you disable button, it looks like disabled but it is clickable and when clicked, it will execute the underlying command. So it is better to enable/disable the command which in turn will automatically make the relevant button/drop down enabled/disabled

if (editor.pramukhime.hasHelp()) {
	// button is enabled but not "selected/pushed"
	editor.getCommand( 'PramukhIMEHelp' ).setState(CKEDITOR.TRISTATE_OFF);
} else {
	// button is disabled
	editor.getCommand( 'PramukhIMEHelp' ).setState(CKEDITOR.TRISTATE_DISABLED);
}

Save reference of a button to perform action later on

// in the "init" function, the variable togglelanguagebutton is defined. Before this code, command PramukhIMEToggleLanguage should have been added 
editor.ui.addButton('PramukhIMEToggleLanguage',
	{
		label: editor.lang.pramukhime.plugin_title,
		command: 'PramukhIMEToggleLanguage',
		toolbar: 'pramukhime', // specify toolbar name
		onRender: function () { 
			togglelanguagebutton = this; // use this variable to check if button exists and then perform action (like change icon)
		}
	});

Dynamically change button icon

// variable pramukhimehelpbutton stores button reference. $ represents underlying DOM element (and not CKEditor element). firstChild represents "span" tag on which class should be set
// Here I am setting a class to an HTML element. All the classes are defined in a custom css which was already loaded in DOM
var data = editor.pramukhime.getLanguage();
CKEDITOR.document.getById( pramukhimehelpbutton._.id ).$.firstChild.className = 'cke_button_icon cke_button__pramukhimehelp_icon pi-' 
				+ data.kb + '-' + data.language + '-help';

Add a button to open a dialog

As mentioned earlier, button is executing a command. So we need to create/define a dialog, create dialog command and create button to execute dialog command.

// in the "init" function.
// define dialog 
CKEDITOR.dialog.add('PramukhIMEResetSettingsDialog', function (editor) {
	return {
		title: editor.lang.pramukhime.reset_setting_title,
		minWidth:200,
		minHeight:50
		contents: [
			{
				elements: [
					{
						type:'html',
						html: editor.lang.pramukhime.reset_settings_desc
					}
				]
			}
		]
	};
});

// create dialog command which is available in wysiwyg and source mode. I had to force the state to TRISTATE_OFF, otherwise it would make the button "selected/pushed"
editor.addCommand('PramukhIMEResetSettings', CKEDITOR.tools.extend(new CKEDITOR.dialogCommand('PramukhIMEResetSettingsDialog'), { modes: { wysiwyg: 1, source: 1}, state:CKEDITOR.TRISTATE_OFF }));

// add a button which executes dialog command
editor.ui.addButton('PramukhIMEResetSettings', {
	label: editor.lang.pramukhime.reset_settings_title,
	command: 'PramukhIMEResetSettings',
	toolbar: 'pramukhime'
});

Add a dropdown/Combo box

// in the "init" function.
editor.ui.addRichCombo('pramukhime',
	{
		command: 'PramukhIME', // command property is not needed but we want to somehow reference this object so it is passed
		label: 'Xlated label', 
		title: 'Xlated title', 
		toolbar: 'pramukhime',
		modes: { wysiwyg: 1, source: 1 },
		panel: {
			css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
			multiSelect: false,
			attributes: { 'aria-label': 'Xlated title'}
		},
		init: function () {
			// Add values to the drop down
			// Adds a group (non selectable)
			this.startGroup('Indic Languages');
			// add languages to the drop down
			// 1st param = value, 2nd param = text, 3rd param = tooltip
			this.add('pramukhindic:sanskrit', 'Sanskrit', 'Sanskrit');
			this.add('pramukhindic:gujarati', 'Gujarati', 'Gujarati');
			
			this.commit();
		},
		onClick: function (value) {
			console.log("selected value is:" + value);
		},

		onRender: function () {
			// any custom code after rendering
		}
	}
);

Dynamically change dialog title

Once CKEditor dialogs are created, they are there forever. For the first time only, they are created. When it is shown subsequent times, it uses the previous instance that was already created. So if the dialog title requires change, we need to explicitly change the title. In my plugin, based on the selected language, I have to show the language name in the title.

// in the "init" function.
CKEDITOR.dialog.add('PramukhIMESettingsDialog', function (editor) {
	return {
		title: xlation.settings_title, // this is a default title which will be changed in "onShow"
		minWidth:200,
		minHeight:50,
		onShow: function() {
			// dynamically change title
			this.parts.title.setHtml('my translated title');
		}
		contents: [
			{
				elements: [
					{
						type:'html',
						html: xlation.reset_settings_desc
					}
				]
			}
		]
	};
});

Create a tabbed dialog

Following code gives 3 tabbed dialog. It automatically creates 3 tabs and makes the first tab visible.

// in the "init" function.
CKEDITOR.dialog.add('PramukhIMEHelpDialog', function (editor) {
	return {
		title: 'My Translated Dialog Title',
		minWidth: 570,
		minHeight: 450,
		contents:
		[
			{
				id: 'quick_help',
				label: 'Xlation Quick Help',
				elements:
				[
					{
						'type': 'html',
						html: '<div>my quick help goes here</div>'
					}
				]
			},
			{
				id: 'detailed_help',
				label: 'Xlation Detailed Help',
				elements:
				[
					{
						'type': 'html',
						html: '<div>my detailed help goes here</div>'
					}
				]
			},
			{
				id: 'about',
				label: 'Xlation About',
				elements:
				[
					{
						'type': 'html',
						html: '<div>About me goes here</div>'
					}
				]
			}
		]
	};
});

Dynamically load JavaScript and css files

// in the "init" function.
// load custom javascript
var scripts = [];
scripts.push(self.path + 'js/pramukhime.js');
scripts.push(self.path + 'anyotherfile.js');
CKEDITOR.scriptLoader.load(scripts, myCustomcallbackFunction, this);

// load custom css in the main DOM
CKEDITOR.document.appendStyleSheet(self.path + 'css/mycustom.css');

Get underlying editable element

When we change the CKEditor mode between wysiwyg and source, EVERY time, it recreates the underlying editable area on which we are typing. Please note that the below code will give the visible editable element in the current mode and not the original element which was replaced by CKEditor to show the editor.

// Following code gives 
// mode = wysiwyg, underlying element = textarea, then iframe body
// mode = source, underlying element = textarea, then a textarea to show source code which is separate from underlying element
// inline editor , then underlying content editable element
// The returned element is an object of type CKEditor element.
editor.editable();
// following code gives the DOM raw element out of CKEditor element. i.e. $
editor.editable().$;
Vishal Monpara is a full stack Solution Developer/Architect with 12 years of experience primarily using Microsoft stack. He is currently working in Retail industry and moving 1’s and 0’s from geographically dispersed hard disks to geographically dispersed user’s mind leveraging geographically dispersed team members.

Leave a Reply

Your email address will not be published. Required fields are marked *